diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bac7f4ae7..a3996ad36c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 4.0.0-alpha.2 + +- feat: Email service (#757) + ## 4.0.0-alpha.1 - chore: upgrade frontend 4.0.0-alpha.1 diff --git a/docs/deploy/configuring-unleash.md b/docs/deploy/configuring-unleash.md index b69f7be3b7..d128db1e36 100644 --- a/docs/deploy/configuring-unleash.md +++ b/docs/deploy/configuring-unleash.md @@ -64,6 +64,14 @@ unleash.start(unleashOptions); - **checkVersion** - the checkVersion object deciding where to check for latest version - `url` - The url to check version (Defaults to `https://version.unleash.run`) - Overridable with (`UNLEASH_VERSION_URL`) - `enable` - Whether version checking is enabled (defaults to true) - Overridable with (`CHECK_VERSION`) (if anything other than `true`, does not check) +- **email** - the email object configuring an SMTP server for sending welcome mails and password reset mails + - `host` - The server URL to your SMTP server + - `port` - Which port the SMTP server is running on. Defaults to 465 (Secure SMTP) + - `secure` (boolean) - Whether to use SMTPS or not. + - `sender` - Which email should be set as sender of mails being sent from Unleash? + - **auth** - For now a user/pass object containing auth details for your SMTP server + - `user` - Username for your SMTP server + - `pass` - Password for your SMTP server ### Disabling Auto-Start diff --git a/package.json b/package.json index 6aed1aecdc..baffb6c1d2 100644 --- a/package.json +++ b/package.json @@ -31,16 +31,18 @@ "start": "node ./dist/server.js", "start:google": "node examples/google-auth-unleash.js", "start:dev": "NODE_ENV=development tsc-watch --onSuccess \"node dist/server-dev.js\"", + "copy-templates": "copyfiles -u 1 src/mailtemplates/**/*.mustache dist/", "db-migrate": "db-migrate --migrations-dir ./src/migrations", "lint": "eslint ./src", "build:watch": "tsc -w", - "build": "tsc", + "build": "yarn run copy-templates && tsc --pretty", "prepare": "yarn run build", "test": "yarn build && NODE_ENV=test PORT=4243 ava", "test:docker": "./scripts/docker-postgres.sh", "test:watch": "yarn test --watch", "test:coverage": "nyc --reporter=lcov yarn test", - "test:coverage-report": "nyc report --reporter=text-lcov | coveralls" + "test:coverage-report": "nyc report --reporter=text-lcov | coveralls", + "clean": "rimraf dist/" }, "nyc": { "all": true, @@ -89,6 +91,7 @@ "multer": "^1.4.1", "mustache": "^4.1.0", "node-fetch": "^2.6.1", + "nodemailer": "^6.5.0", "parse-database-url": "^0.3.0", "pg": "^8.0.3", "pkginfo": "^0.4.1", @@ -105,9 +108,11 @@ "@passport-next/passport-google-oauth2": "^1.0.0", "@types/express": "^4.17.11", "@types/node": "^14.0.0", + "@types/nodemailer": "^6.4.1", "@typescript-eslint/eslint-plugin": "^4.15.2", "@typescript-eslint/parser": "^4.15.2", "ava": "^3.7.0", + "copyfiles": "^2.4.1", "coveralls": "^3.1.0", "eslint": "^6.8.0", "eslint-config-airbnb-base": "^14.1.0", @@ -125,6 +130,7 @@ "passport-google-auth": "^1.0.2", "prettier": "^1.19.1", "proxyquire": "^2.1.3", + "rimraf": "^3.0.2", "sinon": "^9.2.4", "source-map-support": "^0.5.19", "superagent": "^6.1.0", diff --git a/src/lib/db/client-applications-store.js b/src/lib/db/client-applications-store.js index 9680a9a56a..d3bbe80733 100644 --- a/src/lib/db/client-applications-store.js +++ b/src/lib/db/client-applications-store.js @@ -20,7 +20,7 @@ const mapRow = row => ({ createdAt: row.created_at, updatedAt: row.updated_at, description: row.description, - strategies: row.strategies, + strategies: row.strategies || [], createdBy: row.created_by, url: row.url, color: row.color, diff --git a/src/lib/options.js b/src/lib/options.js index 720d52dd85..05b2537ffa 100644 --- a/src/lib/options.js +++ b/src/lib/options.js @@ -102,6 +102,16 @@ function defaultOptions() { }, rbac: false, }, + email: { + host: process.env.EMAIL_HOST, + secure: !!process.env.EMAIL_SECURE, + port: process.env.EMAIL_PORT || 465, + sender: process.env.EMAIL_SENDER || 'noreply@unleash-hosted.com', + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASSWORD, + }, + }, }; } diff --git a/src/lib/routes/admin-api/email.js b/src/lib/routes/admin-api/email.js new file mode 100644 index 0000000000..778b3e31e3 --- /dev/null +++ b/src/lib/routes/admin-api/email.js @@ -0,0 +1,52 @@ +import { TemplateFormat } from '../../services/email-service'; +import { handleErrors } from './util'; +import { ADMIN } from '../../permissions'; + +const Controller = require('../controller'); + +class EmailController extends Controller { + constructor(config, { emailService }) { + super(config); + this.emailService = emailService; + this.logger = config.getLogger('routes/admin-api/email'); + this.get('/preview/html/:template', this.getHtmlPreview, ADMIN); + this.get('/preview/text/:template', this.getTextPreview, ADMIN); + } + + async getHtmlPreview(req, res) { + try { + const { template } = req.params; + const ctx = req.query; + const data = await this.emailService.compileTemplate( + template, + TemplateFormat.HTML, + ctx, + ); + res.setHeader('Content-Type', 'text/html'); + res.status(200); + res.send(data); + res.end(); + } catch (e) { + handleErrors(res, this.logger, e); + } + } + + async getTextPreview(req, res) { + try { + const { template } = req.params; + const ctx = req.query; + const data = await this.emailService.compileTemplate( + template, + TemplateFormat.PLAIN, + ctx, + ); + res.setHeader('Content-Type', 'text/plain'); + res.status(200); + res.send(data); + res.end(); + } catch (e) { + handleErrors(res, this.logger, e); + } + } +} +module.exports = EmailController; diff --git a/src/lib/routes/admin-api/email.test.js b/src/lib/routes/admin-api/email.test.js new file mode 100644 index 0000000000..f11624f112 --- /dev/null +++ b/src/lib/routes/admin-api/email.test.js @@ -0,0 +1,64 @@ +'use strict'; + +const test = require('ava'); +const supertest = require('supertest'); +const { EventEmitter } = require('events'); +const store = require('../../../test/fixtures/store'); +const { createServices } = require('../../services'); +const getLogger = require('../../../test/fixtures/no-logger'); +const getApp = require('../../app'); + +const eventBus = new EventEmitter(); + +function getSetup() { + const base = `/random${Math.round(Math.random() * 1000)}`; + const stores = store.createStores(); + const config = { + baseUriPath: base, + stores, + eventBus, + extendedPermissions: false, + customContextFields: [{ name: 'tenantId' }], + getLogger, + }; + + const services = createServices(stores, config); + const app = getApp(config, services); + + return { + base, + request: supertest(app), + }; +} + +test('should render html preview of template', t => { + t.plan(0); + const { request, base } = getSetup(); + return request + .get( + `${base}/api/admin/email/preview/html/reset-password?name=Test%20Test`, + ) + .expect('Content-Type', /html/) + .expect(200) + .expect(res => 'Test Test' in res.body); +}); + +test('should render text preview of template', t => { + t.plan(0); + const { request, base } = getSetup(); + return request + .get( + `${base}/api/admin/email/preview/text/reset-password?name=Test%20Test`, + ) + .expect('Content-Type', /plain/) + .expect(200) + .expect(res => 'Test Test' in res.body); +}); + +test('Requesting a non-existing template should yield 404', t => { + t.plan(0); + const { request, base } = getSetup(); + return request + .get(`${base}/api/admin/email/preview/text/some-non-existing-template`) + .expect(404); +}); diff --git a/src/lib/routes/admin-api/index.js b/src/lib/routes/admin-api/index.js index c418d3601c..ff8063dcc1 100644 --- a/src/lib/routes/admin-api/index.js +++ b/src/lib/routes/admin-api/index.js @@ -15,6 +15,7 @@ const TagController = require('./tag'); const TagTypeController = require('./tag-type'); const AddonController = require('./addon'); const ApiTokenController = require('./api-token-controller'); +const EmailController = require('./email'); const apiDef = require('./api-def.json'); class AdminApi extends Controller { @@ -63,6 +64,7 @@ class AdminApi extends Controller { '/api-tokens', new ApiTokenController(config, services).router, ); + this.app.use('/email', new EmailController(config, services).router); } index(req, res) { diff --git a/src/lib/services/email-service.test.ts b/src/lib/services/email-service.test.ts new file mode 100644 index 0000000000..c901e5628c --- /dev/null +++ b/src/lib/services/email-service.test.ts @@ -0,0 +1,60 @@ +import test from 'ava'; +import { EmailService, TransporterType } from './email-service'; +import noLoggerProvider from '../../test/fixtures/no-logger'; + +test('Can send reset email', async t => { + const emailService = new EmailService( + { + host: '', + port: 587, + secure: false, + auth: { + user: '', + password: '', + }, + sender: 'noreply@getunleash.ai', + transporterType: TransporterType.JSON, + }, + noLoggerProvider, + ); + const content = await emailService.sendResetMail( + 'Some username', + 'test@test.com', + 'abc123', + ); + const message = JSON.parse(content.message); + t.is(message.from.address, 'noreply@getunleash.ai'); + t.is(message.subject, 'Someone has requested to reset your password'); + t.true(message.html.indexOf('Some username') > 0); + t.true(message.text.indexOf('Some username') > 0); + t.true(message.html.indexOf('abc123') > 0); + t.true(message.text.indexOf('abc123') > 0); +}); + +test('Can send welcome mail', async t => { + const emailService = new EmailService( + { + host: '', + port: 9999, + secure: false, + sender: 'noreply@getunleash.ai', + auth: { + user: '', + password: '', + }, + transporterType: TransporterType.JSON, + }, + noLoggerProvider, + ); + const content = await emailService.sendGettingStartedMail( + 'Some username', + 'test@test.com', + 'abc123456', + ); + const message = JSON.parse(content.message); + t.is(message.from.address, 'noreply@getunleash.ai'); + t.is( + message.subject, + 'Welcome to Unleash. Please configure your password.', + ); +}); diff --git a/src/lib/services/email-service.ts b/src/lib/services/email-service.ts new file mode 100644 index 0000000000..60baee40dc --- /dev/null +++ b/src/lib/services/email-service.ts @@ -0,0 +1,173 @@ +import { createTransport, SentMessageInfo, Transporter } from 'nodemailer'; +import Mustache from 'mustache'; +import path from 'path'; +import { readFileSync, existsSync } from 'fs'; +import { Logger, LogProvider } from '../logger'; +import NotFoundError from '../error/notfound-error'; + +export interface IAuthOptions { + user: string; + password: string; +} + +export enum TemplateFormat { + HTML = 'html', + PLAIN = 'plain', +} + +export enum TransporterType { + SMTP = 'smtp', + JSON = 'json', +} + +export interface IEmailOptions { + host: string; + port: number; + secure: boolean; + sender: string; + auth: IAuthOptions; + transporterType: TransporterType; +} + +const RESET_MAIL_SUBJECT = 'Someone has requested to reset your password'; +const GETTING_STARTED_SUBJECT = + 'Welcome to Unleash. Please configure your password.'; + +export class EmailService { + private logger: Logger; + + private mailer?: Transporter; + + private readonly sender: string; + + constructor(email: IEmailOptions | undefined, getLogger: LogProvider) { + this.logger = getLogger('services/email-service.ts'); + if (email) { + this.sender = email.sender; + if (email.transporterType === TransporterType.JSON) { + this.mailer = createTransport({ jsonTransport: true }); + } else { + const connectionString = `${email.auth.user}:${email.auth.password}@${email.host}:${email.port}`; + this.mailer = email.secure + ? createTransport(`smtps://${connectionString}`) + : createTransport(`smtp://${connectionString}`); + } + this.logger.info( + `Initialized transport to ${email.host} on port ${email.port} with user: ${email.auth.user}`, + ); + } else { + this.sender = 'not-configured'; + this.mailer = undefined; + } + } + + async sendResetMail( + name: string, + recipient: string, + resetLink: string, + ): Promise { + if (this.mailer !== undefined) { + const year = new Date().getFullYear(); + const bodyHtml = await this.compileTemplate( + 'reset-password', + TemplateFormat.HTML, + { + resetLink, + name, + year, + }, + ); + const bodyText = await this.compileTemplate( + 'reset-password', + TemplateFormat.PLAIN, + { + resetLink, + name, + year, + }, + ); + const email = { + from: this.sender, + to: recipient, + subject: RESET_MAIL_SUBJECT, + html: bodyHtml, + text: bodyText, + }; + return this.mailer.sendMail(email); + } + return new Promise(res => { + this.logger.warn( + 'No mailer is configured. Please read the docs on how to configure an emailservice', + ); + res({}); + }); + } + + async sendGettingStartedMail( + name: string, + recipient: string, + passwordLink: string, + ): Promise { + if (this.mailer !== undefined) { + const year = new Date().getFullYear(); + const context = { passwordLink, name, year }; + const bodyHtml = await this.compileTemplate( + 'getting-started', + TemplateFormat.HTML, + context, + ); + const bodyText = await this.compileTemplate( + 'getting-started', + TemplateFormat.PLAIN, + context, + ); + const email = { + from: this.sender, + to: recipient, + subject: GETTING_STARTED_SUBJECT, + html: bodyHtml, + text: bodyText, + }; + return this.mailer.sendMail(email); + } + return new Promise(res => { + this.logger.warn( + 'No mailer is configured. Please read the docs on how to configure an EmailService', + ); + res({}); + }); + } + + private async compileTemplate( + templateName: string, + format: TemplateFormat, + context: any, + ): Promise { + try { + const template = this.resolveTemplate(templateName, format); + return await Promise.resolve(Mustache.render(template, context)); + } catch (e) { + this.logger.info(`Could not find template ${templateName}`); + return Promise.reject(e); + } + } + + private resolveTemplate( + templateName: string, + format: TemplateFormat, + ): string { + let topPath = path.resolve('mailtemplates'); + if (!existsSync(topPath)) { + topPath = path.resolve('dist', 'mailtemplates'); + } + const template = path.join( + topPath, + templateName, + `${templateName}.${format}.mustache`, + ); + if (existsSync(template)) { + return readFileSync(template, 'utf-8'); + } + throw new NotFoundError('Could not find template'); + } +} diff --git a/src/lib/services/index.js b/src/lib/services/index.js index 1800206f10..5c15b8a394 100644 --- a/src/lib/services/index.js +++ b/src/lib/services/index.js @@ -8,6 +8,7 @@ const StrategyService = require('./strategy-service'); const AddonService = require('./addon-service'); const ContextService = require('./context-service'); const VersionService = require('./version-service'); +const { EmailService } = require('./email-service'); const { AccessService } = require('./access-service'); const { ApiTokenService } = require('./api-token-service'); @@ -28,6 +29,7 @@ module.exports.createServices = (stores, config) => { const contextService = new ContextService(stores, config); const versionService = new VersionService(stores, config); const apiTokenService = new ApiTokenService(stores, config); + const emailService = new EmailService(config.email, config.getLogger); return { accessService, @@ -42,5 +44,6 @@ module.exports.createServices = (stores, config) => { contextService, versionService, apiTokenService, + emailService, }; }; diff --git a/src/mailtemplates/getting-started/getting-started.html.mustache b/src/mailtemplates/getting-started/getting-started.html.mustache new file mode 100644 index 0000000000..7a6826908d --- /dev/null +++ b/src/mailtemplates/getting-started/getting-started.html.mustache @@ -0,0 +1,21 @@ + + + + + + Welcome to Unleash - {{ name }} + + +
+ Welcome to Unleash +
+
+ First step: Setup your password by visiting {{ passwordLink }} + Second step: Visit your instance at {{ unleashUrl }} +
+ +
+ © {{ year }} - Unleash +
+ + diff --git a/src/mailtemplates/getting-started/getting-started.plain.mustache b/src/mailtemplates/getting-started/getting-started.plain.mustache new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/mailtemplates/reset-password/reset-password.html.mustache b/src/mailtemplates/reset-password/reset-password.html.mustache new file mode 100644 index 0000000000..c4bc7b8f77 --- /dev/null +++ b/src/mailtemplates/reset-password/reset-password.html.mustache @@ -0,0 +1,22 @@ + + + + + + Reset your password {{ name }} + + +
+ +
+
+ Someone has requested a reset of your password. If this was you, great, click here {{resetLink}} + If this was not you, you might want to check your password still works +
+ + + + diff --git a/src/mailtemplates/reset-password/reset-password.plain.mustache b/src/mailtemplates/reset-password/reset-password.plain.mustache new file mode 100644 index 0000000000..7b03bed5b0 --- /dev/null +++ b/src/mailtemplates/reset-password/reset-password.plain.mustache @@ -0,0 +1,4 @@ +Hello {{ name }} +Someone has requested a reset of your password. +If this was you, great, visit "{{ resetLink }}" to reset your password. +If this was not you, you might want to check your password still works. diff --git a/yarn.lock b/yarn.lock index abea101f83..80bbf37907 100644 --- a/yarn.lock +++ b/yarn.lock @@ -575,6 +575,13 @@ resolved "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz" integrity sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g== +"@types/nodemailer@^6.4.1": + version "6.4.1" + resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.1.tgz#31f96f4410632f781d3613bd1f4293649e423f75" + integrity sha512-8081UY/0XTTDpuGqCnDc8IY+Q3DSg604wB3dBH0CaZlj4nZWHWuxtZ3NRZ9c9WUrz1Vfm6wioAUnqL3bsh49uQ== + dependencies: + "@types/node" "*" + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz" @@ -1359,6 +1366,15 @@ cliui@^7.0.0: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + clone-response@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz" @@ -1621,6 +1637,19 @@ copy-descriptor@^0.1.0: resolved "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +copyfiles@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-2.4.1.tgz#d2dcff60aaad1015f09d0b66e7f0f1c5cd3c5da5" + integrity sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg== + dependencies: + glob "^7.0.5" + minimatch "^3.0.3" + mkdirp "^1.0.4" + noms "0.0.0" + through2 "^2.0.1" + untildify "^4.0.0" + yargs "^16.1.0" + core-js@^3.0.0: version "3.8.3" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0" @@ -2975,7 +3004,7 @@ glob-to-regexp@^0.4.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -3372,6 +3401,11 @@ inherits@2, inherits@2.0.3, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +inherits@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.7" resolved "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz" @@ -4374,7 +4408,7 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== -minimatch@^3.0.4: +minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -4401,6 +4435,11 @@ mkdirp@0.x.x, mkdirp@^0.5.1, mkdirp@~0.5.0: dependencies: minimist "^1.2.5" +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + module-not-found-error@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz" @@ -4540,6 +4579,19 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" +nodemailer@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.5.0.tgz#d12c28d8d48778918e25f1999d97910231b175d9" + integrity sha512-Tm4RPrrIZbnqDKAvX+/4M+zovEReiKlEXWDzG4iwtpL9X34MJY+D5LnQPH/+eghe8DLlAVshHAJZAZWBGhkguw== + +noms@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/noms/-/noms-0.0.0.tgz#da8ebd9f3af9d6760919b27d9cdc8092a7332859" + integrity sha1-2o69nzr51nYJGbJ9nNyAkqczKFk= + dependencies: + inherits "^2.0.1" + readable-stream "~1.0.31" + normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz" @@ -5386,7 +5438,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.2.2: +readable-stream@^2.2.2, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -5408,6 +5460,16 @@ readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@~1.0.31: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readdirp@~3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz" @@ -5621,7 +5683,7 @@ rimraf@2.x.x: dependencies: glob "^7.1.3" -rimraf@^3.0.0: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -6271,6 +6333,14 @@ text-table@^0.2.0: resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +through2@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1: version "2.3.8" resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" @@ -6563,6 +6633,11 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + update-notifier@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz" @@ -6802,7 +6877,7 @@ xdg-basedir@^4.0.0: resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== -xtend@^4.0.0: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== @@ -6817,6 +6892,11 @@ y18n@^5.0.1: resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.1.tgz" integrity sha512-/jJ831jEs4vGDbYPQp4yGKDYPSCCEQ45uZWJHE1AoYBzqdZi8+LDWas0z4HrmJXmKdpFsTiowSHXdxyFhpmdMg== +y18n@^5.0.5: + version "5.0.7" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.7.tgz#0c514aba53fc40e2db911aeb8b51566a3374efe7" + integrity sha512-oOhslryvNcA1lB9WYr+M6TMyLkLg81Dgmyb48ZDU0lvR+5bmNDTMz7iobM1QXooaLhbbrcHrlNaABhI6Vo6StQ== + yallist@^3.0.2: version "3.1.1" resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" @@ -6840,6 +6920,11 @@ yargs-parser@^20.0.0: resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.0.tgz" integrity sha512-2agPoRFPoIcFzOIp6656gcvsg2ohtscpw2OINr/q46+Sq41xz2OYLqx5HRHabmFU1OARIPAYH5uteICE7mn/5A== +yargs-parser@^20.2.2: + version "20.2.7" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" + integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== + yargs@^15.0.2, yargs@^15.3.1, yargs@^15.4.1: version "15.4.1" resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz" @@ -6870,6 +6955,19 @@ yargs@^16.0.3: y18n "^5.0.1" yargs-parser "^20.0.0" +yargs@^16.1.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"