Add Notification Service (#19236)
This adds a notification service to Kibana that can be used to send asynchronous notifications, such as sending email and Slack messages, which are intended to be configured via a combination of the `kibana.yml` and Kibana keystore. Once configured, the actions are automatically added to the notification service and can be invoked via the server using the `notificationService` singleton or HTTP to send it directly. See the included README for more details.
This commit is contained in:
parent
37487a8472
commit
e6a88e000d
|
@ -21,6 +21,7 @@ import { licenseManagement } from './plugins/license_management';
|
|||
import { cloud } from './plugins/cloud';
|
||||
import { indexManagement } from './plugins/index_management';
|
||||
import { consoleExtensions } from './plugins/console_extensions';
|
||||
import { notifications } from './plugins/notifications';
|
||||
|
||||
module.exports = function (kibana) {
|
||||
return [
|
||||
|
@ -40,6 +41,7 @@ module.exports = function (kibana) {
|
|||
licenseManagement(kibana),
|
||||
cloud(kibana),
|
||||
indexManagement(kibana),
|
||||
consoleExtensions(kibana)
|
||||
consoleExtensions(kibana),
|
||||
notifications(kibana),
|
||||
];
|
||||
};
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
"@elastic/numeral": "2.3.2",
|
||||
"@kbn/datemath": "link:../packages/kbn-datemath",
|
||||
"@kbn/ui-framework": "link:../packages/kbn-ui-framework",
|
||||
"@slack/client": "^4.2.2",
|
||||
"angular-paging": "2.2.1",
|
||||
"angular-resource": "1.4.9",
|
||||
"angular-sanitize": "1.4.9",
|
||||
|
@ -122,6 +123,7 @@
|
|||
"moment-duration-format": "^1.3.0",
|
||||
"moment-timezone": "^0.5.14",
|
||||
"ngreact": "^0.5.1",
|
||||
"nodemailer": "^4.6.4",
|
||||
"object-hash": "1.2.0",
|
||||
"path-match": "1.2.4",
|
||||
"pdfmake": "0.1.33",
|
||||
|
|
53
x-pack/plugins/notifications/config.js
Normal file
53
x-pack/plugins/notifications/config.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* User-configurable settings for xpack.notifications via configuration schema
|
||||
*
|
||||
* @param {Object} Joi - HapiJS Joi module that allows for schema validation
|
||||
* @return {Object} config schema
|
||||
*/
|
||||
export const config = (Joi) => {
|
||||
const { array, boolean, number, object, string } = Joi;
|
||||
|
||||
return object({
|
||||
enabled: boolean().default(true),
|
||||
email: object({
|
||||
enabled: boolean().default(false),
|
||||
smtp: object({
|
||||
host: string().default('localhost'),
|
||||
port: number().default(25),
|
||||
require_tls: boolean().default(false),
|
||||
pool: boolean().default(false),
|
||||
auth: object({
|
||||
username: string(),
|
||||
password: string()
|
||||
}).default(),
|
||||
}).default(),
|
||||
defaults: object({
|
||||
from: string(),
|
||||
to: array().single().items(string()),
|
||||
cc: array().single().items(string()),
|
||||
bcc: array().single().items(string()),
|
||||
}).default(),
|
||||
}).default(),
|
||||
slack: object({
|
||||
enabled: boolean().default(false),
|
||||
token: string().required(),
|
||||
defaults: object({
|
||||
channel: string(),
|
||||
as_user: boolean().default(false),
|
||||
icon_emoji: string(),
|
||||
icon_url: string(),
|
||||
link_names: boolean().default(true),
|
||||
mrkdwn: boolean().default(true),
|
||||
unfurl_links: boolean().default(true),
|
||||
unfurl_media: boolean().default(true),
|
||||
username: string(),
|
||||
}).default(),
|
||||
})
|
||||
}).default();
|
||||
};
|
24
x-pack/plugins/notifications/index.js
Normal file
24
x-pack/plugins/notifications/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { init } from './init';
|
||||
import { config } from './config';
|
||||
|
||||
/**
|
||||
* Invokes plugin modules to instantiate the Notification plugin for Kibana
|
||||
*
|
||||
* @param kibana {Object} Kibana plugin instance
|
||||
* @return {Object} Notification Kibana plugin object
|
||||
*/
|
||||
export const notifications = (kibana) => new kibana.Plugin({
|
||||
require: ['kibana', 'xpack_main'],
|
||||
id: 'notifications',
|
||||
configPrefix: 'xpack.notifications',
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
init,
|
||||
config,
|
||||
});
|
38
x-pack/plugins/notifications/init.js
Normal file
38
x-pack/plugins/notifications/init.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
notificationService,
|
||||
createEmailAction,
|
||||
createSlackAction,
|
||||
LoggerAction,
|
||||
} from './server';
|
||||
import { notificationServiceSendRoute } from './server/routes/api/v1/notifications';
|
||||
|
||||
/**
|
||||
* Initialize the Action Service with various actions provided by X-Pack, when configured.
|
||||
*
|
||||
* @param server {Object} HapiJS server instance
|
||||
*/
|
||||
export function init(server) {
|
||||
const config = server.config();
|
||||
|
||||
// the logger
|
||||
notificationService.setAction(new LoggerAction({ server }));
|
||||
|
||||
if (config.get('xpack.notifications.email.enabled')) {
|
||||
notificationService.setAction(createEmailAction(server));
|
||||
}
|
||||
|
||||
if (config.get('xpack.notifications.slack.enabled')) {
|
||||
notificationService.setAction(createSlackAction(server));
|
||||
}
|
||||
|
||||
notificationServiceSendRoute(server, notificationService);
|
||||
|
||||
// expose the notification service for other plugins
|
||||
server.expose('notificationService', notificationService);
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EmailAction } from './email_action';
|
||||
|
||||
/**
|
||||
* Create a Nodemailer transporter options object from the config.
|
||||
*
|
||||
* @param {Object} config The server configuration.
|
||||
* @return {Object} An object that configures Nodemailer.
|
||||
*/
|
||||
export function optionsFromConfig(config) {
|
||||
return {
|
||||
host: config.get('xpack.notifications.email.smtp.host'),
|
||||
port: config.get('xpack.notifications.email.smtp.port'),
|
||||
requireTLS: config.get('xpack.notifications.email.smtp.require_tls'),
|
||||
pool: config.get('xpack.notifications.email.smtp.pool'),
|
||||
auth: {
|
||||
user: config.get('xpack.notifications.email.smtp.auth.username'),
|
||||
pass: config.get('xpack.notifications.email.smtp.auth.password'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Nodemailer defaults object from the config.
|
||||
*
|
||||
* Defaults include things like the default "from" email address.
|
||||
*
|
||||
* @param {Object} config The server configuration.
|
||||
* @return {Object} An object that configures Nodemailer on a per-message basis.
|
||||
*/
|
||||
export function defaultsFromConfig(config) {
|
||||
return {
|
||||
from: config.get('xpack.notifications.email.defaults.from'),
|
||||
to: config.get('xpack.notifications.email.defaults.to'),
|
||||
cc: config.get('xpack.notifications.email.defaults.cc'),
|
||||
bcc: config.get('xpack.notifications.email.defaults.bcc'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Email Action based on the configuration.
|
||||
*
|
||||
* @param {Object} server The server object.
|
||||
* @return {EmailAction} A new email action based on the kibana.yml configuration.
|
||||
*/
|
||||
export function createEmailAction(server, { _options = optionsFromConfig, _defaults = defaultsFromConfig } = { }) {
|
||||
const config = server.config();
|
||||
|
||||
const options = _options(config);
|
||||
const defaults = _defaults(config);
|
||||
|
||||
return new EmailAction({ server, options, defaults });
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EmailAction } from './email_action';
|
||||
import {
|
||||
createEmailAction,
|
||||
defaultsFromConfig,
|
||||
optionsFromConfig,
|
||||
} from './create_email_action';
|
||||
|
||||
describe('create_email_action', () => {
|
||||
|
||||
test('optionsFromConfig uses config without modification', () => {
|
||||
const get = key => {
|
||||
const suffixes = [
|
||||
'host',
|
||||
'port',
|
||||
'require_tls',
|
||||
'pool',
|
||||
'auth.username',
|
||||
'auth.password',
|
||||
];
|
||||
const value = suffixes.find(suffix => {
|
||||
return `xpack.notifications.email.smtp.${suffix}` === key;
|
||||
});
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error(`Unknown config key used ${key}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
expect(optionsFromConfig({ get })).toEqual({
|
||||
host: 'host',
|
||||
port: 'port',
|
||||
requireTLS: 'require_tls',
|
||||
pool: 'pool',
|
||||
auth: {
|
||||
user: 'auth.username',
|
||||
pass: 'auth.password',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('defaultsFromConfig uses config without modification', () => {
|
||||
const get = key => {
|
||||
const suffixes = [
|
||||
'from',
|
||||
'to',
|
||||
'cc',
|
||||
'bcc',
|
||||
];
|
||||
const value = suffixes.find(suffix => {
|
||||
return `xpack.notifications.email.defaults.${suffix}` === key;
|
||||
});
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error(`Unknown config key used ${key}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
expect(defaultsFromConfig({ get })).toEqual({
|
||||
from: 'from',
|
||||
to: 'to',
|
||||
cc: 'cc',
|
||||
bcc: 'bcc',
|
||||
});
|
||||
});
|
||||
|
||||
test('createEmailAction', async () => {
|
||||
const config = { };
|
||||
const server = { config: jest.fn().mockReturnValue(config) };
|
||||
const _options = jest.fn().mockReturnValue({ options: true });
|
||||
const defaults = { defaults: true };
|
||||
const _defaults = jest.fn().mockReturnValue(defaults);
|
||||
|
||||
const action = createEmailAction(server, { _options, _defaults });
|
||||
|
||||
expect(action instanceof EmailAction).toBe(true);
|
||||
expect(action.defaults).toBe(defaults);
|
||||
|
||||
expect(server.config).toHaveBeenCalledTimes(1);
|
||||
expect(server.config).toHaveBeenCalledWith();
|
||||
expect(_options).toHaveBeenCalledTimes(1);
|
||||
expect(_options).toHaveBeenCalledWith(config);
|
||||
expect(_defaults).toHaveBeenCalledTimes(1);
|
||||
expect(_defaults).toHaveBeenCalledWith(config);
|
||||
});
|
||||
|
||||
});
|
103
x-pack/plugins/notifications/server/email/email_action.js
Normal file
103
x-pack/plugins/notifications/server/email/email_action.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
import { Action, ActionResult } from '../';
|
||||
|
||||
export const EMAIL_ACTION_ID = 'xpack-notifications-email';
|
||||
|
||||
/**
|
||||
* Email Action enables generic sending of emails, when configured.
|
||||
*/
|
||||
export class EmailAction extends Action {
|
||||
|
||||
/**
|
||||
* Create a new Action capable of sending emails.
|
||||
*
|
||||
* @param {Object} server Kibana server object.
|
||||
* @param {Object} options Configuration options for Nodemailer.
|
||||
* @param {Object} defaults Default fields used when sending emails.
|
||||
* @param {Object} _nodemailer Exposed for tests.
|
||||
*/
|
||||
constructor({ server, options, defaults = { }, _nodemailer = nodemailer }) {
|
||||
super({ server, id: EMAIL_ACTION_ID, name: 'Email' });
|
||||
|
||||
this.transporter = _nodemailer.createTransport(options, defaults);
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
getMissingFields(notification) {
|
||||
const missingFields = [];
|
||||
|
||||
if (!Boolean(this.defaults.to) && !Boolean(notification.to)) {
|
||||
missingFields.push({
|
||||
field: 'to',
|
||||
name: 'To',
|
||||
type: 'email',
|
||||
});
|
||||
}
|
||||
|
||||
if (!Boolean(this.defaults.from) && !Boolean(notification.from)) {
|
||||
missingFields.push({
|
||||
field: 'from',
|
||||
name: 'From',
|
||||
type: 'email',
|
||||
});
|
||||
}
|
||||
|
||||
if (!Boolean(notification.subject)) {
|
||||
missingFields.push({
|
||||
field: 'subject',
|
||||
name: 'Subject',
|
||||
type: 'text',
|
||||
});
|
||||
}
|
||||
|
||||
if (!Boolean(notification.markdown)) {
|
||||
missingFields.push({
|
||||
field: 'markdown',
|
||||
name: 'Body',
|
||||
type: 'markdown',
|
||||
});
|
||||
}
|
||||
|
||||
return missingFields;
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
// this responds with a boolean 'true' response, otherwise throws an Error
|
||||
const response = await this.transporter.verify();
|
||||
|
||||
return new ActionResult({
|
||||
message: `Email action SMTP configuration has been verified.`,
|
||||
response: {
|
||||
verified: response
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async doPerformAction(notification) {
|
||||
// Note: This throws an Error upon failure
|
||||
const response = await this.transporter.sendMail({
|
||||
// email routing
|
||||
from: notification.from,
|
||||
to: notification.to,
|
||||
cc: notification.cc,
|
||||
bcc: notification.bcc,
|
||||
// email content
|
||||
subject: notification.subject,
|
||||
html: notification.markdown,
|
||||
text: notification.markdown,
|
||||
});
|
||||
|
||||
return new ActionResult({
|
||||
message: `Sent email for '${notification.subject}'.`,
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
166
x-pack/plugins/notifications/server/email/email_action.test.js
Normal file
166
x-pack/plugins/notifications/server/email/email_action.test.js
Normal file
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ActionResult } from '../';
|
||||
import { EMAIL_ACTION_ID, EmailAction } from './email_action';
|
||||
|
||||
describe('EmailAction', () => {
|
||||
|
||||
const server = { };
|
||||
const options = { options: true };
|
||||
const defaults = { defaults: true };
|
||||
const transporter = {
|
||||
// see beforeEach
|
||||
};
|
||||
const _nodemailer = {
|
||||
// see beforeEach
|
||||
};
|
||||
|
||||
let action;
|
||||
|
||||
beforeEach(() => {
|
||||
transporter.verify = jest.fn();
|
||||
transporter.sendMail = jest.fn();
|
||||
_nodemailer.createTransport = jest.fn().mockReturnValue(transporter);
|
||||
|
||||
action = new EmailAction({ server, options, defaults, _nodemailer });
|
||||
});
|
||||
|
||||
test('id and name to be from constructor', () => {
|
||||
expect(action.getId()).toBe(EMAIL_ACTION_ID);
|
||||
expect(action.getName()).toBe('Email');
|
||||
expect(action.transporter).toBe(transporter);
|
||||
|
||||
expect(_nodemailer.createTransport).toHaveBeenCalledTimes(1);
|
||||
expect(_nodemailer.createTransport).toHaveBeenCalledWith(options, defaults);
|
||||
});
|
||||
|
||||
describe('getMissingFields', () => {
|
||||
|
||||
test('returns missing fields', () => {
|
||||
const to = { field: 'to', name: 'To', type: 'email' };
|
||||
const from = { field: 'from', name: 'From', type: 'email' };
|
||||
const subject = { field: 'subject', name: 'Subject', type: 'text' };
|
||||
const markdown = { field: 'markdown', name: 'Body', type: 'markdown' };
|
||||
|
||||
const missing = [
|
||||
{ defaults: { }, notification: { }, missing: [ to, from, subject, markdown, ], },
|
||||
{ defaults: { }, notification: { from: 'b@c.co', subject: 'subject', markdown: 'body', }, missing: [ to, ], },
|
||||
{ defaults: { from: 'b@c.co', }, notification: { subject: 'subject', markdown: 'body', }, missing: [ to, ], },
|
||||
{ defaults: { }, notification: { to: 'a@b.co', subject: 'subject', markdown: 'body', }, missing: [ from, ], },
|
||||
{ defaults: { to: 'a@b.co', }, notification: { subject: 'subject', markdown: 'body', }, missing: [ from, ], },
|
||||
{ defaults: { }, notification: { to: 'a@b.co', from: 'b@c.co', markdown: 'body', }, missing: [ subject, ], },
|
||||
{ defaults: { }, notification: { to: 'a@b.co', from: 'b@c.co', subject: 'subject', }, missing: [ markdown, ], },
|
||||
];
|
||||
|
||||
missing.forEach(check => {
|
||||
const newDefaultsAction = new EmailAction({ server, defaults: check.defaults, _nodemailer });
|
||||
|
||||
expect(newDefaultsAction.getMissingFields(check.notification)).toEqual(check.missing);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns [] when all fields exist', () => {
|
||||
const exists = [
|
||||
{ defaults: { }, notification: { to: 'a@b.co', from: 'b@c.co', subject: 'subject', markdown: 'body', }, },
|
||||
{ defaults: { to: 'a@b.co', }, notification: { from: 'b@c.co', subject: 'subject', markdown: 'body', }, },
|
||||
{ defaults: { from: 'b@c.co', }, notification: { to: 'a@b.co', subject: 'subject', markdown: 'body', }, },
|
||||
{ defaults: { to: 'a@b.co', from: 'b@c.co', }, notification: { subject: 'subject', markdown: 'body', }, },
|
||||
];
|
||||
|
||||
exists.forEach(check => {
|
||||
const newDefaultsAction = new EmailAction({ server, defaults: check.defaults, _nodemailer });
|
||||
|
||||
expect(newDefaultsAction.getMissingFields(check.notification)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('doPerformHealthCheck', () => {
|
||||
|
||||
test('rethrows Error for failure', async () => {
|
||||
const error = new Error('TEST - expected');
|
||||
|
||||
transporter.verify.mockRejectedValue(error);
|
||||
|
||||
await expect(action.doPerformHealthCheck())
|
||||
.rejects
|
||||
.toThrow(error);
|
||||
|
||||
expect(transporter.verify).toHaveBeenCalledTimes(1);
|
||||
expect(transporter.verify).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('returns ActionResult for success', async () => {
|
||||
transporter.verify.mockResolvedValue(true);
|
||||
|
||||
const result = await action.doPerformHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch('Email action SMTP configuration has been verified.');
|
||||
expect(result.getResponse()).toEqual({
|
||||
verified: true
|
||||
});
|
||||
|
||||
expect(transporter.verify).toHaveBeenCalledTimes(1);
|
||||
expect(transporter.verify).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('doPerformAction', () => {
|
||||
const email = { subject: 'email', markdown: 'body' };
|
||||
|
||||
test('rethrows Error for failure', async () => {
|
||||
const error = new Error('TEST - expected');
|
||||
|
||||
transporter.sendMail.mockRejectedValue(error);
|
||||
|
||||
await expect(action.doPerformAction(email))
|
||||
.rejects
|
||||
.toThrow(error);
|
||||
|
||||
expect(transporter.sendMail).toHaveBeenCalledTimes(1);
|
||||
expect(transporter.sendMail).toHaveBeenCalledWith({
|
||||
to: undefined,
|
||||
from: undefined,
|
||||
cc: undefined,
|
||||
bcc: undefined,
|
||||
subject: email.subject,
|
||||
html: email.markdown,
|
||||
text: email.markdown,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns ActionResult for success', async () => {
|
||||
const response = { fake: true };
|
||||
|
||||
transporter.sendMail.mockResolvedValue(response);
|
||||
|
||||
const result = await action.doPerformAction(email);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch(`Sent email for '${email.subject}'.`);
|
||||
expect(result.getResponse()).toBe(response);
|
||||
|
||||
expect(transporter.sendMail).toHaveBeenCalledTimes(1);
|
||||
expect(transporter.sendMail).toHaveBeenCalledWith({
|
||||
to: undefined,
|
||||
from: undefined,
|
||||
cc: undefined,
|
||||
bcc: undefined,
|
||||
subject: email.subject,
|
||||
html: email.markdown,
|
||||
text: email.markdown,
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
7
x-pack/plugins/notifications/server/email/index.js
Normal file
7
x-pack/plugins/notifications/server/email/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { createEmailAction } from './create_email_action';
|
14
x-pack/plugins/notifications/server/index.js
Normal file
14
x-pack/plugins/notifications/server/index.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export {
|
||||
notificationService,
|
||||
Action,
|
||||
ActionResult,
|
||||
} from './service';
|
||||
export { createEmailAction } from './email';
|
||||
export { createSlackAction } from './slack';
|
||||
export { LoggerAction } from './logger';
|
7
x-pack/plugins/notifications/server/logger/index.js
Normal file
7
x-pack/plugins/notifications/server/logger/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { LoggerAction } from './logger_action';
|
42
x-pack/plugins/notifications/server/logger/logger_action.js
Normal file
42
x-pack/plugins/notifications/server/logger/logger_action.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Action, ActionResult } from '../';
|
||||
|
||||
export const LOGGER_ACTION_ID = 'xpack-notifications-logger';
|
||||
|
||||
/**
|
||||
* Logger Action enables generic logging of information into Kibana's logs.
|
||||
*
|
||||
* This is mostly useful for debugging.
|
||||
*/
|
||||
export class LoggerAction extends Action {
|
||||
|
||||
constructor({ server }) {
|
||||
super({ server, id: LOGGER_ACTION_ID, name: 'Log' });
|
||||
}
|
||||
|
||||
getMissingFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
return new ActionResult({
|
||||
message: `Logger action is always usable.`,
|
||||
response: { },
|
||||
});
|
||||
}
|
||||
|
||||
async doPerformAction(notification) {
|
||||
this.server.log([LOGGER_ACTION_ID, 'info'], notification);
|
||||
|
||||
return new ActionResult({
|
||||
message: 'Logged data returned as response.',
|
||||
response: notification
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ActionResult } from '../';
|
||||
import { LOGGER_ACTION_ID, LoggerAction } from './logger_action';
|
||||
|
||||
describe('LoggerAction', () => {
|
||||
|
||||
const action = new LoggerAction({ server: { } });
|
||||
|
||||
test('id and name to be from constructor', () => {
|
||||
expect(action.id).toBe(LOGGER_ACTION_ID);
|
||||
expect(action.name).toBe('Log');
|
||||
});
|
||||
|
||||
test('getMissingFields to return []', () => {
|
||||
expect(action.getMissingFields()).toEqual([]);
|
||||
});
|
||||
|
||||
test('doPerformHealthCheck returns ActionResult', async () => {
|
||||
const result = await action.doPerformHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch('Logger action is always usable.');
|
||||
expect(result.getResponse()).toEqual({ });
|
||||
});
|
||||
|
||||
test('doPerformAction logs and returns ActionResult', async () => {
|
||||
const notification = { fake: true };
|
||||
|
||||
const logger = jest.fn();
|
||||
const server = { log: logger };
|
||||
const action = new LoggerAction({ server });
|
||||
|
||||
const result = await action.doPerformAction(notification);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch('Logged data returned as response.');
|
||||
expect(result.getResponse()).toBe(notification);
|
||||
|
||||
expect(logger).toHaveBeenCalledTimes(1);
|
||||
expect(logger).toHaveBeenCalledWith([LOGGER_ACTION_ID, 'info'], notification);
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { notificationServiceSendRoute } from './notify';
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import { wrap } from 'boom';
|
||||
|
||||
/**
|
||||
* Check the incoming request parameters to see if the action should be allowed to fire.
|
||||
*
|
||||
* @param {Object|null} action The action selected by the user.
|
||||
* @param {String} actionId The ID of the requested action from the user.
|
||||
* @param {Object} data The incoming data from the user.
|
||||
* @returns {Object|null} The error object, or null if no error.
|
||||
*/
|
||||
export function checkForErrors(action, actionId, data) {
|
||||
if (action === null) {
|
||||
return {
|
||||
message: `Unrecognized action: '${actionId}'.`,
|
||||
};
|
||||
} else {
|
||||
let validLicense = false;
|
||||
|
||||
try {
|
||||
validLicense = action.isLicenseValid();
|
||||
} catch (e) {
|
||||
// validLicense === false
|
||||
}
|
||||
|
||||
if (validLicense === false) {
|
||||
return {
|
||||
message: `Unable to perform '${action.name}' action due to the current license.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const fields = action.getMissingFields(data);
|
||||
|
||||
if (fields.length !== 0) {
|
||||
return {
|
||||
message: `Unable to perform '${action.name}' action due to missing required fields.`,
|
||||
fields
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to send the {@code data} as a notification.
|
||||
*
|
||||
* @param {Object} server Kibana server object.
|
||||
* @param {NotificationService} notificationService The notification service singleton.
|
||||
* @param {String} actionId The specified action's ID.
|
||||
* @param {Function} data The notification data to send via the specified action.
|
||||
* @param {Function} reply The response function from the server route.
|
||||
* @param {Function} _checkForErrors Exposed for testing.
|
||||
*/
|
||||
export async function sendNotification(server, notificationService, actionId, data, reply, { _checkForErrors = checkForErrors } = { }) {
|
||||
const action = notificationService.getActionForId(actionId);
|
||||
const error = _checkForErrors(action, actionId, data);
|
||||
|
||||
if (error === null) {
|
||||
return action.performAction(data)
|
||||
.then(result => reply(result.toJson()))
|
||||
.catch(err => reply(wrap(err))); // by API definition, this should never happen as performAction isn't allow to throw errrors
|
||||
}
|
||||
|
||||
server.log(['actions', 'error'], error.message);
|
||||
|
||||
reply({
|
||||
status_code: 400,
|
||||
ok: false,
|
||||
message: `Error: ${error.message}`,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification Service route to perform actions (aka send data).
|
||||
*/
|
||||
export function notificationServiceSendRoute(server, notificationService) {
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: '/api/notifications/v1/notify',
|
||||
config: {
|
||||
validate: {
|
||||
payload: Joi.object({
|
||||
action: Joi.string().required(),
|
||||
data: Joi.object({
|
||||
from: Joi.string(),
|
||||
to: Joi.string(),
|
||||
subject: Joi.string().required(),
|
||||
markdown: Joi.string(),
|
||||
}).required()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: (req, reply) => {
|
||||
const actionId = req.payload.action;
|
||||
const data = req.payload.data;
|
||||
|
||||
sendNotification(server, notificationService, actionId, data, reply);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { checkForErrors, sendNotification } from './notify';
|
||||
import { wrap } from 'boom';
|
||||
|
||||
describe('notifications/routes/send', () => {
|
||||
|
||||
const id = 'notifications-test';
|
||||
const notification = { fake: true };
|
||||
|
||||
describe('checkForErrors', () => {
|
||||
|
||||
it('returns unrecognized action for null action', () => {
|
||||
expect(checkForErrors(null, id, { })).toEqual({
|
||||
message: `Unrecognized action: '${id}'.`,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns invalid license if license check throws an error', () => {
|
||||
const action = {
|
||||
name: 'Test Action',
|
||||
isLicenseValid: () => {
|
||||
throw new Error();
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkForErrors(action, id, { })).toEqual({
|
||||
message: `Unable to perform '${action.name}' action due to the current license.`,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns invalid license if license is invalid', () => {
|
||||
const action = {
|
||||
name: 'Test Action',
|
||||
isLicenseValid: () => false,
|
||||
};
|
||||
|
||||
expect(checkForErrors(action, id, { })).toEqual({
|
||||
message: `Unable to perform '${action.name}' action due to the current license.`,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns fields related to missing data', () => {
|
||||
const fields = [ { field: 1 } ];
|
||||
const action = {
|
||||
name: 'Test Action',
|
||||
isLicenseValid: () => true,
|
||||
getMissingFields: (data) => {
|
||||
expect(data).toBe(notification);
|
||||
|
||||
return fields;
|
||||
},
|
||||
};
|
||||
|
||||
const error = checkForErrors(action, id, notification);
|
||||
|
||||
expect(error).toEqual({
|
||||
message: `Unable to perform '${action.name}' action due to missing required fields.`,
|
||||
fields
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null if action is usable', () => {
|
||||
const notification = { fake: true };
|
||||
const action = {
|
||||
name: 'Test Action',
|
||||
isLicenseValid: () => true,
|
||||
getMissingFields: (data) => {
|
||||
expect(data).toBe(notification);
|
||||
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
expect(checkForErrors(action, id, notification)).toBeNull();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('sendNotification', () => {
|
||||
|
||||
it('replies with error object for bad request', async () => {
|
||||
const error = {
|
||||
message: 'TEST - expected',
|
||||
fields: [ { fake: 1 } ],
|
||||
};
|
||||
const action = { };
|
||||
const server = {
|
||||
log: jest.fn(),
|
||||
};
|
||||
const notificationService = {
|
||||
getActionForId: jest.fn().mockReturnValue(action),
|
||||
};
|
||||
const checkForErrors = jest.fn().mockReturnValue(error);
|
||||
const reply = jest.fn();
|
||||
|
||||
await sendNotification(server, notificationService, id, notification, reply, { _checkForErrors: checkForErrors });
|
||||
|
||||
expect(notificationService.getActionForId).toHaveBeenCalledTimes(1);
|
||||
expect(notificationService.getActionForId).toHaveBeenCalledWith(id);
|
||||
expect(checkForErrors).toHaveBeenCalledTimes(1);
|
||||
expect(checkForErrors).toHaveBeenCalledWith(action, id, notification);
|
||||
expect(server.log).toHaveBeenCalledTimes(1);
|
||||
expect(server.log).toHaveBeenCalledWith(['actions', 'error'], error.message);
|
||||
|
||||
expect(reply).toHaveBeenCalledTimes(1);
|
||||
expect(reply).toHaveBeenCalledWith({
|
||||
status_code: 400,
|
||||
ok: false,
|
||||
message: `Error: ${error.message}`,
|
||||
error,
|
||||
});
|
||||
});
|
||||
|
||||
it('replies with action result JSON', async () => {
|
||||
const response = { ok: true, message: 'Test' };
|
||||
const result = {
|
||||
toJson: () => response,
|
||||
};
|
||||
const action = {
|
||||
performAction: jest.fn().mockReturnValue(Promise.resolve(result))
|
||||
};
|
||||
const server = { };
|
||||
const notificationService = {
|
||||
getActionForId: jest.fn().mockReturnValue(action),
|
||||
};
|
||||
const checkForErrors = jest.fn().mockReturnValue(null);
|
||||
const reply = jest.fn();
|
||||
|
||||
await sendNotification(server, notificationService, id, notification, reply, { _checkForErrors: checkForErrors });
|
||||
|
||||
expect(notificationService.getActionForId).toHaveBeenCalledTimes(1);
|
||||
expect(notificationService.getActionForId).toHaveBeenCalledWith(id);
|
||||
expect(checkForErrors).toHaveBeenCalledTimes(1);
|
||||
expect(checkForErrors).toHaveBeenCalledWith(action, id, notification);
|
||||
|
||||
expect(reply).toHaveBeenCalledTimes(1);
|
||||
expect(reply).toHaveBeenCalledWith(response);
|
||||
});
|
||||
|
||||
it('replies with unexpected result error', async () => {
|
||||
const error = new Error();
|
||||
const action = {
|
||||
performAction: jest.fn().mockReturnValue(Promise.reject(error))
|
||||
};
|
||||
const server = { };
|
||||
const notificationService = {
|
||||
getActionForId: jest.fn().mockReturnValue(action),
|
||||
};
|
||||
const checkForErrors = jest.fn().mockReturnValue(null);
|
||||
const reply = jest.fn();
|
||||
|
||||
await sendNotification(server, notificationService, id, notification, reply, { _checkForErrors: checkForErrors });
|
||||
|
||||
expect(notificationService.getActionForId).toHaveBeenCalledTimes(1);
|
||||
expect(notificationService.getActionForId).toHaveBeenCalledWith(id);
|
||||
expect(checkForErrors).toHaveBeenCalledTimes(1);
|
||||
expect(checkForErrors).toHaveBeenCalledWith(action, id, notification);
|
||||
|
||||
expect(reply).toHaveBeenCalledTimes(1);
|
||||
expect(reply).toHaveBeenCalledWith(wrap(error));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
575
x-pack/plugins/notifications/server/service/README.md
Normal file
575
x-pack/plugins/notifications/server/service/README.md
Normal file
|
@ -0,0 +1,575 @@
|
|||
# Notification Service / Actions
|
||||
|
||||
Use this service to send notifications to users. An example of a notification is sending an email or
|
||||
explicitly adding something to the Kibana log.
|
||||
|
||||
Notifications are inherently asynchronous actions because of the likelihood that any notification is
|
||||
interacting with a remote server or service.
|
||||
|
||||
## Referencing the Notification Service
|
||||
|
||||
Note: Both of these may change in the future.
|
||||
|
||||
### Server Side
|
||||
|
||||
```js
|
||||
const notificationService = server.plugins.notifications.notificationService;
|
||||
|
||||
const action = notificationService.getActionForId('xpack-notifications-logger');
|
||||
const result = action.performAction({
|
||||
arbitrary: 'payload',
|
||||
can: 'have multiple',
|
||||
fields: [ 1, 2, 3 ]
|
||||
});
|
||||
```
|
||||
|
||||
### HTTP
|
||||
|
||||
```http
|
||||
POST /api/notifications/v1/notify
|
||||
{
|
||||
"action": "xpack-notifications-logger",
|
||||
"data": {
|
||||
"arbitrary": "payload",
|
||||
"can": "have multiple",
|
||||
"fields": [ 1, 2, 3 ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Interfaces
|
||||
|
||||
There are two interfaces that are important from this package. `NotificationService`, which is exposed as
|
||||
a singleton from the plugin: `server.plugins.notifications.notificationService`. And `Action`, which
|
||||
provides an abstract Javascript `class` to implement new `Action`s.
|
||||
|
||||
### NotificationService Interface
|
||||
|
||||
The `NotificationService` currently has four methods defined with very distinct purposes:
|
||||
|
||||
1. `setAction` is intended for plugin authors to add actions that do not exist with the basic notifications
|
||||
service.
|
||||
2. `removeAction` is intended for replacing existing plugins (e.g., augmenting another action).
|
||||
3. `getActionForId` enables explicitly fetching an action by its known ID.
|
||||
4. `getActionsForData` enables discovering compatible actions given an arbitrary set of data.
|
||||
|
||||
Note: Mutating the Notification Service should generally only be done based on very specific reasons,
|
||||
such as plugin initialization or the user dynamically configuring a service's availability (e.g.,
|
||||
if we support a secure data store, the user could theoretically provide authentication details for an email
|
||||
server).
|
||||
|
||||
It is also possible that the user will want to configure multiple variants of the same action, such as
|
||||
multiple email notifications with differing defaults. In that case, the action's ID may need to be
|
||||
reconsidered.
|
||||
|
||||
#### `setAction()`
|
||||
|
||||
This is the only way to add an `Action` instance. Instances are expected to be extensions of the `Action`
|
||||
class defined here. If the provided action already exists, then the old one is removed.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
notificationService.setAction(action);
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `action` | Action | The unique action to support. |
|
||||
|
||||
###### Returns
|
||||
|
||||
Nothing.
|
||||
|
||||
##### Example
|
||||
|
||||
Create a simple logging action that can be triggered generically.
|
||||
|
||||
```js
|
||||
class LoggerAction extends Action {
|
||||
|
||||
constructor({ server }) {
|
||||
super({ server, id: 'xpack-notifications-logger', name: 'Log' });
|
||||
}
|
||||
|
||||
getMissingFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
return new ActionResult({
|
||||
message: `Logger action is always usable.`,
|
||||
response: { },
|
||||
});
|
||||
}
|
||||
|
||||
async doPerformAction(notification) {
|
||||
this.server.log(['logger', 'info'], notification);
|
||||
|
||||
return new ActionResult({
|
||||
message: 'Logged data returned as response.',
|
||||
response: notification
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// It's possible that someone may choose to make the LoggerAction's log level configurable, so
|
||||
// replacing it could be done by re-setting it, which means any follow-on usage would use the new level
|
||||
// (or, possibly, you could create different actions entirely for different levels)
|
||||
notificationService.setAction(new LoggerAction({ server }));
|
||||
```
|
||||
|
||||
#### `removeAction()`
|
||||
|
||||
Remove an action that has been set.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
const action = notificationService.removeAction(actionId);
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | String | ID of the action to remove. |
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| Action \| null | The action that was removed. `null` otherwise. |
|
||||
|
||||
##### Example
|
||||
|
||||
```js
|
||||
const action = notificationService.removeAction('xpack-notifications-logger');
|
||||
|
||||
if (action !== null) {
|
||||
// removed; otherwise it didn't exist (maybe it was already removed)
|
||||
}
|
||||
```
|
||||
|
||||
#### `getActionForId()`
|
||||
|
||||
Retrieve a specific `Action` from the Notification Service.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
const action = notificationService.getActionForId(actionId);
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | String | ID of the action to retrieve. |
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| Action \| null | The action that was requested. `null` otherwise. |
|
||||
|
||||
##### Example
|
||||
|
||||
```js
|
||||
// In this case, the ID is known from the earlier example
|
||||
const action = notificationService.getActionForId('xpack-notifications-logger');
|
||||
|
||||
if (action !== null) {
|
||||
// otherwise it didn't exist
|
||||
}
|
||||
```
|
||||
|
||||
#### `getActionsForData()`
|
||||
|
||||
Retrieve any `Action`s from the Notification Service that accept the supplied data, which is useful for
|
||||
discovery.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
const actions = notificationService.getActionsForData(notification);
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `notification` | Object | Payload to send notification. |
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| Action[] | The actions that accept a subset of the data. Empty array otherwise. |
|
||||
|
||||
##### Example
|
||||
|
||||
```js
|
||||
// In this case, the ID is known from the earlier example
|
||||
const actions = notificationService.getActionsForData({
|
||||
arbitrary: 'payload',
|
||||
can: 'have multiple',
|
||||
fields: [ 1, 2, 3 ]
|
||||
});
|
||||
|
||||
if (action.length !== 0) {
|
||||
// otherwise nothing matches
|
||||
}
|
||||
```
|
||||
|
||||
### Action Interface
|
||||
|
||||
From the perspective of developers that want to make use of `Action`s, there are three relevant methods:
|
||||
|
||||
1. `getMissingFields` provides an array of fields as well as the expected data type that did not exist in the
|
||||
supplied data.
|
||||
2. `performHealthCheck` attempts to perform a health check against the actions backing service.
|
||||
3. `performAction` attempts to perform the purpose of the action (e.g., send the email) using the supplied
|
||||
data.
|
||||
|
||||
For developers to create new `Action`s, there are three related methods:
|
||||
|
||||
1. `getMissingFields` provides an array of fields as well as the expected data type that did not exist in the
|
||||
supplied data.
|
||||
2. `doPerformHealthCheck` attempts to perform a health check against the actions backing service.
|
||||
- `performHealthCheck` invokes this method and wraps it in order to catch `Error`s.
|
||||
3. `doPerformAction` attempts to perform the purpose of the action (e.g., send the email) using the supplied
|
||||
data.
|
||||
- `performAction` invokes this method and wraps it in order to catch `Error`s.
|
||||
|
||||
Every method, excluding `getMissingFields`, is asynchronous.
|
||||
|
||||
#### `getMissingFields()`
|
||||
|
||||
This method enables the building of "Sharing"-style UIs that allow the same payload to be shared across
|
||||
many different actions. This is the same approach taken in iOS and Android sharing frameworks.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
action.getMissingFields({
|
||||
arbitrary: 'payload',
|
||||
can: 'have multiple',
|
||||
fields: [ 1, 2, 3 ]
|
||||
});
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `notification` | Object | The data that you want to try to use with the action. |
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| Object[] | The fields that were not present in the `notification` object. Empty array otherwise. |
|
||||
|
||||
The object definition should match:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `field` | String | The JSON field name that was expected. |
|
||||
| `name` | String | The user-readable name of the field (e.g., for a generated UI). |
|
||||
| `type` | String | The type of data (`email`, `text`, `markdown`, `number`, `date`, `boolean`). |
|
||||
|
||||
NOTE: This method _never_ throws an `Error`.
|
||||
|
||||
##### Example
|
||||
|
||||
Create a simple action whose fields can be checked automatically.
|
||||
|
||||
```js
|
||||
// Action Users
|
||||
const action = notificationService.getActionForId(actionId);
|
||||
const fields = action.getMissingFields({
|
||||
arbitrary: 'payload',
|
||||
can: 'have multiple',
|
||||
fields: [ 1, 2, 3 ]
|
||||
});
|
||||
|
||||
if (fields.length !== 0) {
|
||||
// there's some fields missing so this action should not be used yet
|
||||
}
|
||||
|
||||
// Action Creators
|
||||
class FakeAction extends Action {
|
||||
|
||||
constructor({ server, defaults = { } }) {
|
||||
super({ server, id: 'xpack-notifications-fake', name: 'Fake' });
|
||||
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
getMissingFields(notification) {
|
||||
const missingFields = [];
|
||||
|
||||
if (!Boolean(this.defaults.to) && !Boolean(notification.to)) {
|
||||
missingFields.push({
|
||||
field: 'to',
|
||||
name: 'To',
|
||||
type: 'email',
|
||||
});
|
||||
}
|
||||
|
||||
return missingFields;
|
||||
}
|
||||
|
||||
// ... other methods ...
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
#### `performHealthCheck()`
|
||||
|
||||
This method enables the health status of third party services to be polled. The current approach only allows
|
||||
an `Action`'s health to be in a boolean state: either it's up and expected to work, or it's down.
|
||||
|
||||
The health check is interesting because some third party services that we anticipate supporting are
|
||||
inherently untrustworthy when it comes to supporting health checks (e.g., email servers). Therefore, it
|
||||
should not be expected that the health check will block usage of actions -- only provide feedback to the
|
||||
user, which we can ideally provide in a future notification center within the UI.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
const result = await action.performHealthCheck();
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
None.
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| ActionResult | The result of the health check. |
|
||||
|
||||
An `ActionResult` defines a few methods:
|
||||
|
||||
| Method | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `isOk()` | Boolean | `true` if there was no error and it is believed to be "up". |
|
||||
| `getError()` | Object \| undefined | JSON error object. |
|
||||
| `getResponse()` | Object \| undefined | JSON response from the server. If the server returns a field, it should be wrapped. |
|
||||
| `getMessage()` | String | Human readable message describing the state. |
|
||||
|
||||
NOTE: This method _never_ throws an `Error`.
|
||||
|
||||
##### Example
|
||||
|
||||
Create a simple action whose fields can be checked automatically.
|
||||
|
||||
```js
|
||||
const action = notificationService.getActionForId(actionId);
|
||||
const result = await action.performHealthCheck();
|
||||
|
||||
if (result.isOk()) {
|
||||
// theoretically the action is in a usable state
|
||||
} else {
|
||||
// theoretically the action is not in a usable state (but some services may have broken health checks!)
|
||||
}
|
||||
```
|
||||
|
||||
#### `performAction()`
|
||||
|
||||
This method enables the actual usage of the `Action` for action users.
|
||||
|
||||
##### Syntax
|
||||
|
||||
```js
|
||||
const result = await action.performAction({
|
||||
arbitrary: 'payload',
|
||||
can: 'have multiple',
|
||||
fields: [ 1, 2, 3 ]
|
||||
});
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `notification` | Object | The data that you want to try to use with the action. |
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| ActionResult | The result of the health check. |
|
||||
|
||||
An `ActionResult` defines a few methods:
|
||||
|
||||
| Method | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `isOk()` | Boolean | `true` if there was no error and it is believed to be "up". |
|
||||
| `getError()` | Object \| undefined | JSON error object. |
|
||||
| `getResponse()` | Object \| undefined | JSON response from the server. If the server returns a field, it should be wrapped. |
|
||||
| `getMessage()` | String | Human readable message describing the state. |
|
||||
|
||||
NOTE: This method _never_ throws an `Error`.
|
||||
|
||||
##### Example
|
||||
|
||||
Create a simple action whose fields can be checked automatically.
|
||||
|
||||
```js
|
||||
const action = notificationService.getActionForId(actionId);
|
||||
const result = await action.performAction({
|
||||
arbitrary: 'payload',
|
||||
can: 'have multiple',
|
||||
fields: [ 1, 2, 3 ]
|
||||
});
|
||||
|
||||
if (result.isOk()) {
|
||||
// theoretically the action is in a usable state
|
||||
} else {
|
||||
// theoretically the action is not in a usable state (but some services may have broken health checks!)
|
||||
}
|
||||
```
|
||||
|
||||
#### `doPerformHealthCheck()`
|
||||
|
||||
This method is for `Action` creators. This performs the actual work to check the health of the action's
|
||||
associated service as best as possible.
|
||||
|
||||
This method should be thought of as a `protected` method only and it should never be called directly
|
||||
outside of tests.
|
||||
|
||||
##### Syntax
|
||||
|
||||
Do not call this method directly outside of tests.
|
||||
|
||||
```js
|
||||
const result = await action.doPerformHealthCheck();
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
None.
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| ActionResult | The result of the health check. |
|
||||
|
||||
An `ActionResult` defines a few methods:
|
||||
|
||||
| Method | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `isOk()` | Boolean | `true` if there was no error and it is believed to be "up". |
|
||||
| `getError()` | Object \| undefined | JSON error object. |
|
||||
| `getResponse()` | Object \| undefined | JSON response from the server. If the server returns a field, it should be wrapped. |
|
||||
| `getMessage()` | String | Human readable message describing the state. |
|
||||
|
||||
NOTE: This method can throw an `Error` in lieu of returning an `ActionResult`.
|
||||
|
||||
##### Example
|
||||
|
||||
Create a simple action whose health status can be checked automatically.
|
||||
|
||||
```js
|
||||
class FakeAction extends Action {
|
||||
|
||||
constructor({ server }) {
|
||||
super({ server, id: 'xpack-notifications-fake', name: 'Fake' });
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
// this responds with a boolean 'true' response, otherwise throws an Error
|
||||
const response = await this.transporter.verify();
|
||||
|
||||
return new ActionResult({
|
||||
message: `Fake action configuration has been verified.`,
|
||||
response: {
|
||||
verified: true
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ... other methods ...
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
#### `doPerformAction()`
|
||||
|
||||
This method is for `Action` creators. This performs the actual function of the action.
|
||||
|
||||
This method should be thought of as a `protected` method only and it should never be called directly
|
||||
outside of tests.
|
||||
|
||||
##### Syntax
|
||||
|
||||
Do not call this method directly outside of tests.
|
||||
|
||||
```js
|
||||
const result = await action.doPerformAction();
|
||||
```
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `notification` | Object | The data that you want to try to use with the action. |
|
||||
|
||||
###### Returns
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| ActionResult | The result of the health check. |
|
||||
|
||||
An `ActionResult` defines a few methods:
|
||||
|
||||
| Method | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `isOk()` | Boolean | `true` if there was no error and it is believed to be "up". |
|
||||
| `getError()` | Object \| undefined | JSON error object. |
|
||||
| `getResponse()` | Object \| undefined | JSON response from the server. If the server returns a field, it should be wrapped. |
|
||||
| `getMessage()` | String | Human readable message describing the state. |
|
||||
|
||||
NOTE: This method can throw an `Error` in lieu of returning an `ActionResult`.
|
||||
|
||||
##### Example
|
||||
|
||||
Create a simple action whose health status can be checked automatically.
|
||||
|
||||
```js
|
||||
class FakeAction extends Action {
|
||||
|
||||
constructor({ server }) {
|
||||
super({ server, id: 'xpack-notifications-fake', name: 'Fake' });
|
||||
}
|
||||
|
||||
async doPerformAction(notification) {
|
||||
// Note: This throws an Error upon failure
|
||||
const response = await this.transporter.sendMail({
|
||||
from: notification.from,
|
||||
to: notification.to,
|
||||
cc: notification.cc,
|
||||
bcc: notification.bcc,
|
||||
subject: notification.subject,
|
||||
html: notification.markdown,
|
||||
text: notification.markdown,
|
||||
});
|
||||
|
||||
return new ActionResult({
|
||||
message: `Success! Sent email for '${notification.subject}'.`,
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
// ... other methods ...
|
||||
|
||||
}
|
||||
```
|
143
x-pack/plugins/notifications/server/service/action.js
Normal file
143
x-pack/plugins/notifications/server/service/action.js
Normal file
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ActionResult } from './action_result';
|
||||
|
||||
/**
|
||||
* Actions represent a singular, generic "action", such as "Send to Email".
|
||||
*
|
||||
* Note: Implementations of Action are inherently server-side operations. It may or may not be desiable to fire
|
||||
* these actions from the UI (triggering a server-side call), but these should be called for such a purpose.
|
||||
*/
|
||||
export class Action {
|
||||
|
||||
/**
|
||||
* Create a new Action.
|
||||
*
|
||||
* The suggested ID is the name of the plugin that provides it, and the unique portion.
|
||||
* For example: "core-email" if core provided email.
|
||||
*
|
||||
* @param {Object} server The Kibana server object.
|
||||
* @param {String} id The unique identifier for the action.
|
||||
* @param {String} name User-friendly name for the action.
|
||||
*/
|
||||
constructor({ server, id, name }) {
|
||||
this.server = server;
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique ID of the Action.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user-friendly name of the Action.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this action can use the {@code notification}. This is useful if you use the action service
|
||||
* generically, such as using it from a generic UI.
|
||||
*
|
||||
* This is intended to be a simple check of the {@code notification}, rather than an asynchronous action.
|
||||
*
|
||||
* @param {Object} notification The notification data to use.
|
||||
* @return {Array} Array defining missing fields. Empty if none.
|
||||
*/
|
||||
getMissingFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementers must override to perform the health check.
|
||||
*
|
||||
* This should not be called directly outside of tests to ensure that any error is wrapped properly.
|
||||
*
|
||||
* Note: Some services do not provide consistent, reliable health checks, such as email. As such,
|
||||
* implementers must weigh the nature of false negatives versus the utility of having this check.
|
||||
*
|
||||
* @return {Promise} The result of the health check, which must be an {@code ActionResult}.
|
||||
* @throws {Error} if there is an unexpected failure occurs.
|
||||
*/
|
||||
async doPerformHealthCheck() {
|
||||
throw new Error(`[doPerformHealthCheck] is not implemented for '${this.name}' action.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the action can be used to the best of the ability of the service that it is using.
|
||||
*
|
||||
* @return {Promise} Always an {@code ActionResult}.
|
||||
*/
|
||||
async performHealthCheck() {
|
||||
try {
|
||||
return await this.doPerformHealthCheck();
|
||||
} catch (error) {
|
||||
return new ActionResult({
|
||||
message: `Unable to perform '${this.name}' health check: ${error.message}.`,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementers must override to perform the action using the {@code notification}.
|
||||
*
|
||||
* This should not be called directly to ensure that any error is wrapped properly.
|
||||
*
|
||||
* @param {Object} notification The notification data to use.
|
||||
* @return {Promise} The result of the action, which must be a {@code ActionResult}.
|
||||
* @throws {Error} if the method is not implemented or an unexpected failure occurs.
|
||||
*/
|
||||
async doPerformAction(notification) {
|
||||
throw new Error(`[doPerformAction] is not implemented for '${this.name}' action: ${JSON.stringify(notification)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the current license allows actions.
|
||||
*
|
||||
* @return {Boolean} true when it is usable
|
||||
* @throws {Error} if there is an unexpected issue checking the license
|
||||
*/
|
||||
isLicenseValid() {
|
||||
return this.server.plugins.xpack_main.info.license.isNotBasic();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the action using the {@code notification}.
|
||||
*
|
||||
* Actions automatically fail if the license check fails.
|
||||
*
|
||||
* Note to implementers: override doPerformAction instead of this method to help guarantee proper handling.
|
||||
*
|
||||
* @param {Object} notification The notification data to use.
|
||||
* @return {Promise} The result of the action, which must be a {@code ActionResult}.
|
||||
*/
|
||||
async performAction(notification) {
|
||||
try {
|
||||
if (!this.isLicenseValid()) {
|
||||
throw new Error(`The current license does not allow '${this.name}' action.`);
|
||||
}
|
||||
|
||||
return await this.doPerformAction(notification);
|
||||
} catch (error) {
|
||||
return new ActionResult({
|
||||
message: `Unable to perform '${this.name}' action: ${error.message}.`,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
242
x-pack/plugins/notifications/server/service/action.test.js
Normal file
242
x-pack/plugins/notifications/server/service/action.test.js
Normal file
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Action } from './action';
|
||||
import { ActionResult } from './action_result';
|
||||
|
||||
describe('Action', () => {
|
||||
|
||||
const server = { };
|
||||
const id = 'notifications-test';
|
||||
const unimplementedName = 'Unimplemented';
|
||||
const throwsErrorName = 'Throws Error';
|
||||
const passThruName = 'Test Action';
|
||||
const action = new Action({ server, id, name: unimplementedName });
|
||||
const notification = {
|
||||
fake: true,
|
||||
};
|
||||
|
||||
test('id and name to be from constructor', () => {
|
||||
expect(action.server).toBe(server);
|
||||
expect(action.getId()).toBe(id);
|
||||
expect(action.getName()).toBe(unimplementedName);
|
||||
});
|
||||
|
||||
test('getMissingFields returns an empty array', () => {
|
||||
expect(action.getMissingFields()).toEqual([]);
|
||||
expect(action.getMissingFields(notification)).toEqual([]);
|
||||
});
|
||||
|
||||
test('doPerformHealthChecks throws error indicating that it is not implemented', async () => {
|
||||
await expect(action.doPerformHealthCheck())
|
||||
.rejects
|
||||
.toThrow(`[doPerformHealthCheck] is not implemented for '${unimplementedName}' action.`);
|
||||
});
|
||||
|
||||
describe('performHealthChecks', () => {
|
||||
|
||||
class ThrowsErrorHealthCheckAction extends Action {
|
||||
constructor() {
|
||||
super({ server: { }, id, name: throwsErrorName });
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
throw new Error('TEST - expected');
|
||||
}
|
||||
}
|
||||
|
||||
class PassThruHealthCheckAction extends Action {
|
||||
constructor(result) {
|
||||
super({ server: { }, id, name: passThruName });
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
||||
test('runs against unimplemented doPerformHealthChecks', async () => {
|
||||
const result = await action.performHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage())
|
||||
.toMatch(new RegExp(`^Unable to perform '${unimplementedName}' health check: \\[doPerformHealthCheck\\] is not.*`));
|
||||
});
|
||||
|
||||
test('runs against failing doPerformHealthChecks', async () => {
|
||||
const failAction = new ThrowsErrorHealthCheckAction();
|
||||
const result = await failAction.performHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage())
|
||||
.toMatch(new RegExp(`^Unable to perform '${throwsErrorName}' health check: TEST - expected`));
|
||||
});
|
||||
|
||||
test('runs against succeeding result', async () => {
|
||||
const expectedResult = new ActionResult({ message: 'Blah', response: { ok: true } });
|
||||
const succeedsAction = new PassThruHealthCheckAction(expectedResult);
|
||||
const result = await succeedsAction.performHealthCheck();
|
||||
|
||||
expect(result).toBe(expectedResult);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('doPerformAction throws error indicating that it is not implemented', async () => {
|
||||
await expect(action.doPerformAction(notification))
|
||||
.rejects
|
||||
.toThrow(`[doPerformAction] is not implemented for '${unimplementedName}' action: {"fake":true}`);
|
||||
});
|
||||
|
||||
describe('isLicenseValid', () => {
|
||||
|
||||
test('server variable is not exposed as expected', () => {
|
||||
expect(() => action.isLicenseValid()).toThrow(Error);
|
||||
});
|
||||
|
||||
test('returns false is license is not valid', () => {
|
||||
const unlicensedServer = {
|
||||
plugins: {
|
||||
xpack_main: {
|
||||
info: {
|
||||
license: {
|
||||
isNotBasic: () => false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const unlicensedAction = new Action({ server: unlicensedServer, id, name: unimplementedName });
|
||||
|
||||
expect(unlicensedAction.isLicenseValid()).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true is license is not valid', () => {
|
||||
const licensedServer = {
|
||||
plugins: {
|
||||
xpack_main: {
|
||||
info: {
|
||||
license: {
|
||||
isNotBasic: () => true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const licensedAction = new Action({ server: licensedServer, id, name: unimplementedName });
|
||||
|
||||
expect(licensedAction.isLicenseValid()).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('performAction', () => {
|
||||
|
||||
// valid license
|
||||
const server = {
|
||||
plugins: {
|
||||
xpack_main: {
|
||||
info: {
|
||||
license: {
|
||||
isNotBasic: () => true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class ThrowsErrorAction extends Action {
|
||||
constructor() {
|
||||
super({ server, id, name: throwsErrorName });
|
||||
}
|
||||
|
||||
async doPerformAction() {
|
||||
throw new Error('TEST - expected');
|
||||
}
|
||||
}
|
||||
|
||||
class PassThruAction extends Action {
|
||||
constructor(result) {
|
||||
super({ server, id, name: passThruName });
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
async doPerformAction() {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
||||
describe('fails license check', () => {
|
||||
|
||||
// handles the case when xpack_main definitions change
|
||||
test('because of bad reference', async () => {
|
||||
// server is an empty object, so a reference fails early in the chain (mostly a way to give devs a way to find this error)
|
||||
const result = await action.performAction(notification);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
});
|
||||
|
||||
test('because license is invalid or basic', async () => {
|
||||
const unlicensedServer = {
|
||||
plugins: {
|
||||
xpack_main: {
|
||||
info: {
|
||||
license: {
|
||||
isNotBasic: () => false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const unlicensedAction = new Action({ server: unlicensedServer, id, name: unimplementedName });
|
||||
const result = await unlicensedAction.performAction(notification);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage())
|
||||
.toMatch(
|
||||
`Unable to perform '${unimplementedName}' action: ` +
|
||||
`The current license does not allow '${unimplementedName}' action.`
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('runs against unimplemented doPerformAction', async () => {
|
||||
const licensedAction = new Action({ server, id, name: unimplementedName });
|
||||
const result = await licensedAction.performAction(notification);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage())
|
||||
.toMatch(new RegExp(`^Unable to perform '${unimplementedName}' action: \\[doPerformAction\\] is not.*`));
|
||||
});
|
||||
|
||||
test('runs against failing doPerformAction', async () => {
|
||||
const failAction = new ThrowsErrorAction();
|
||||
const result = await failAction.performAction(notification);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage())
|
||||
.toMatch(new RegExp(`^Unable to perform '${throwsErrorName}' action: TEST - expected`));
|
||||
});
|
||||
|
||||
test('runs against succeeding result', async () => {
|
||||
const expectedResult = new ActionResult({ message: 'Blah', response: { ok: true } });
|
||||
const succeedsAction = new PassThruAction(expectedResult);
|
||||
const result = await succeedsAction.performAction(notification);
|
||||
|
||||
expect(result).toBe(expectedResult);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
73
x-pack/plugins/notifications/server/service/action_result.js
Normal file
73
x-pack/plugins/notifications/server/service/action_result.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Action Results represent generic, predictable responses from Actions.
|
||||
*/
|
||||
export class ActionResult {
|
||||
|
||||
/**
|
||||
* Create a new Action Result.
|
||||
*
|
||||
* Success is determined by the existence of an error.
|
||||
*
|
||||
* @param {String} message The message to display about the result, presumably in a Toast.
|
||||
* @param {Object|undefined} response The response from the "other" side.
|
||||
* @param {Object|undefined} error The error, if any.
|
||||
*/
|
||||
constructor({ message, response, error }) {
|
||||
this.message = message;
|
||||
this.response = response;
|
||||
this.error = error;
|
||||
this.ok = !Boolean(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error caused by the action.
|
||||
*
|
||||
* @returns {Object|undefined} The error response, or {@code undefined} if no error.
|
||||
*/
|
||||
getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message displayable to the user.
|
||||
*
|
||||
* @returns {String} The message.
|
||||
*/
|
||||
getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw JSON response from the action.
|
||||
*
|
||||
* @returns {Object|undefined} The JSON response.
|
||||
*/
|
||||
getResponse() {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the action succeeded.
|
||||
*
|
||||
* @returns {Boolean} {@code true} for success.
|
||||
*/
|
||||
isOk() {
|
||||
return this.ok;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
ok: this.ok,
|
||||
error: this.error,
|
||||
message: this.message,
|
||||
response: this.response,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ActionResult } from './action_result';
|
||||
|
||||
describe('ActionResult', () => {
|
||||
|
||||
const message = 'this is a message';
|
||||
const response = { other: { side: { response: true } } };
|
||||
const error = { message: `Error: ${message}` };
|
||||
|
||||
const okResult = new ActionResult({ message, response });
|
||||
const notOkResult = new ActionResult({ message, response, error });
|
||||
|
||||
test('getError returns supplied error or undefined', () => {
|
||||
expect(okResult.getError()).toBeUndefined();
|
||||
expect(notOkResult.getError()).toBe(error);
|
||||
});
|
||||
|
||||
test('getMessage returns supplied message', () => {
|
||||
expect(okResult.getMessage()).toBe(message);
|
||||
expect(notOkResult.getMessage()).toBe(message);
|
||||
});
|
||||
|
||||
test('getResponse returns supplied response', () => {
|
||||
expect(okResult.getResponse()).toBe(response);
|
||||
expect(notOkResult.getResponse()).toBe(response);
|
||||
});
|
||||
|
||||
test('isOk returns based on having an error', () => {
|
||||
expect(okResult.isOk()).toBe(true);
|
||||
expect(notOkResult.isOk()).toBe(false);
|
||||
});
|
||||
|
||||
test('toJson', () => {
|
||||
expect(okResult.toJson()).toEqual({
|
||||
ok: true,
|
||||
error: undefined,
|
||||
message,
|
||||
response,
|
||||
});
|
||||
|
||||
expect(notOkResult.toJson()).toEqual({
|
||||
ok: false,
|
||||
error,
|
||||
message,
|
||||
response,
|
||||
});
|
||||
});
|
||||
|
||||
});
|
9
x-pack/plugins/notifications/server/service/index.js
Normal file
9
x-pack/plugins/notifications/server/service/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { notificationService } from './notification_service';
|
||||
export { Action } from './action';
|
||||
export { ActionResult } from './action_result';
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Notification Service represents a service that contains generic "actions", such as "Send to Email"
|
||||
* that are added at startup by plugins to enable notifications to be sent either by the user manually
|
||||
* or via the some scheduled / automated task.
|
||||
*/
|
||||
export class NotificationService {
|
||||
|
||||
constructor() {
|
||||
this.actions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new action to the action service.
|
||||
*
|
||||
* @param {Action} action An implementation of Action.
|
||||
*/
|
||||
setAction = (action) => {
|
||||
this.removeAction(action.id);
|
||||
|
||||
this.actions.push(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an existing action from the action service.
|
||||
*
|
||||
* @param {String} id The ID of the action to remove.
|
||||
* @return {Action} The action that was removed, or null.
|
||||
*/
|
||||
removeAction = (id) => {
|
||||
const index = this.actions.findIndex(action => action.id === id);
|
||||
|
||||
if (index !== -1) {
|
||||
const removedActions = this.actions.splice(index, 1);
|
||||
return removedActions[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action with the specified {@code id}, if any.
|
||||
*
|
||||
* This is useful when you know that an action is provided, such as one provided by your own plugin,
|
||||
* and you want to use it to handle things in a consistent way.
|
||||
*
|
||||
* @param {String} id The ID of the Action.
|
||||
* @return {Action} The Action that matches the ID, or null.
|
||||
*/
|
||||
getActionForId = (id) => {
|
||||
const index = this.actions.findIndex(action => action.id === id);
|
||||
|
||||
if (index !== -1) {
|
||||
return this.actions[index];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actions that will accept the {@code data}.
|
||||
*
|
||||
* @param {Object} data The data object to pass to actions.
|
||||
* @return {Array} An array of Actions that can be used with the data, if any. Empty if none.
|
||||
*/
|
||||
getActionsForData = (data) => {
|
||||
return this.actions.filter(action => {
|
||||
try {
|
||||
return action.getMissingFields(data).length === 0;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A singleton reference to the notification service intended to be used across Kibana.
|
||||
*/
|
||||
export const notificationService = new NotificationService();
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Action } from './action';
|
||||
import { NotificationService } from './notification_service';
|
||||
|
||||
class TestAction extends Action {
|
||||
constructor({ server, id }) {
|
||||
super({ server, id, name: 'TestAction' });
|
||||
}
|
||||
|
||||
getMissingFields() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// always returns a missing field
|
||||
class MissingFieldTestAction extends Action {
|
||||
constructor({ server, id }) {
|
||||
super({ server, id, name: 'MissingFieldTestAction' });
|
||||
}
|
||||
|
||||
getMissingFields() {
|
||||
return [ { field: 'subject', name: 'Subject', type: 'text' } ];
|
||||
}
|
||||
}
|
||||
|
||||
describe('NotificationService', () => {
|
||||
|
||||
const server = { };
|
||||
const actionId = 'notifications-test';
|
||||
const action = new TestAction({ server, id: actionId });
|
||||
|
||||
let notificationService;
|
||||
|
||||
beforeEach(() => {
|
||||
notificationService = new NotificationService();
|
||||
});
|
||||
|
||||
test('initializes with no default actions', () => {
|
||||
expect(notificationService.actions).toEqual([]);
|
||||
});
|
||||
|
||||
describe('setAction', () => {
|
||||
|
||||
test('adds the action', () => {
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.actions[0]).toBe(action);
|
||||
});
|
||||
|
||||
test('removes any action with the same ID first, then adds the action', () => {
|
||||
notificationService.setAction({ id: actionId });
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.actions).toHaveLength(1);
|
||||
expect(notificationService.actions[0]).toBe(action);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('removeAction', () => {
|
||||
|
||||
test('returns null if the action does not exist', () => {
|
||||
expect(notificationService.removeAction(actionId)).toBe(null);
|
||||
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.removeAction('not-' + actionId)).toBe(null);
|
||||
expect(notificationService.actions[0]).toBe(action);
|
||||
});
|
||||
|
||||
test('returns the removed action', () => {
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.removeAction(actionId)).toBe(action);
|
||||
expect(notificationService.actions).toEqual([]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('getActionForId', () => {
|
||||
|
||||
test('returns null if the action does not exist', () => {
|
||||
expect(notificationService.getActionForId(actionId)).toBe(null);
|
||||
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.getActionForId('not-' + actionId)).toBe(null);
|
||||
expect(notificationService.actions[0]).toBe(action);
|
||||
});
|
||||
|
||||
test('returns the action', () => {
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.getActionForId(actionId)).toBe(action);
|
||||
expect(notificationService.actions[0]).toBe(action);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('getActionsForData', () => {
|
||||
|
||||
test('returns [] if no corresponding action exists', () => {
|
||||
expect(notificationService.getActionsForData({})).toEqual([]);
|
||||
|
||||
notificationService.setAction(new MissingFieldTestAction({ server, id: 'always-missing' }));
|
||||
|
||||
expect(notificationService.getActionsForData({})).toEqual([]);
|
||||
expect(notificationService.actions).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('returns the actions that match', () => {
|
||||
notificationService.setAction(action);
|
||||
|
||||
expect(notificationService.getActionsForData({})).toEqual([ action ]);
|
||||
expect(notificationService.actions[0]).toBe(action);
|
||||
|
||||
const otherActionId = 'other-' + actionId;
|
||||
|
||||
notificationService.setAction(new MissingFieldTestAction({ server, id: 'always-missing' }));
|
||||
notificationService.setAction(new TestAction({ server, id: otherActionId }));
|
||||
|
||||
const actions = notificationService.getActionsForData({});
|
||||
|
||||
expect(actions.map(action => action.id)).toEqual([ actionId, otherActionId ]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SlackAction } from './slack_action';
|
||||
|
||||
/**
|
||||
* Create a Slack options object from the config.
|
||||
*
|
||||
* @param {Object} config The server configuration.
|
||||
* @return {Object} An object that configures Slack.
|
||||
*/
|
||||
export function optionsFromConfig(config) {
|
||||
return {
|
||||
token: config.get('xpack.notifications.slack.token')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Slack defaults object from the config.
|
||||
*
|
||||
* Defaults include things like the default channel that messages are posted to.
|
||||
*
|
||||
* @param {Object} config The server configuration.
|
||||
* @return {Object} An object that configures Slack on a per-message basis.
|
||||
*/
|
||||
export function defaultsFromConfig(config) {
|
||||
return {
|
||||
channel: config.get('xpack.notifications.slack.defaults.channel'),
|
||||
as_user: config.get('xpack.notifications.slack.defaults.as_user'),
|
||||
icon_emoji: config.get('xpack.notifications.slack.defaults.icon_emoji'),
|
||||
icon_url: config.get('xpack.notifications.slack.defaults.icon_url'),
|
||||
link_names: config.get('xpack.notifications.slack.defaults.link_names'),
|
||||
mrkdwn: config.get('xpack.notifications.slack.defaults.mrkdwn'),
|
||||
unfurl_links: config.get('xpack.notifications.slack.defaults.unfurl_links'),
|
||||
unfurl_media: config.get('xpack.notifications.slack.defaults.unfurl_media'),
|
||||
username: config.get('xpack.notifications.slack.defaults.username'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Slack Action based on the configuration.
|
||||
*
|
||||
* @param {Object} server The server object.
|
||||
* @return {SlackAction} A new Slack Action based on the kibana.yml configuration.
|
||||
*/
|
||||
export function createSlackAction(server, { _options = optionsFromConfig, _defaults = defaultsFromConfig } = { }) {
|
||||
const config = server.config();
|
||||
|
||||
const options = _options(config);
|
||||
const defaults = _defaults(config);
|
||||
|
||||
return new SlackAction({ server, options, defaults });
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SlackAction } from './slack_action';
|
||||
import {
|
||||
createSlackAction,
|
||||
defaultsFromConfig,
|
||||
optionsFromConfig,
|
||||
} from './create_slack_action';
|
||||
|
||||
describe('create_slack_action', () => {
|
||||
|
||||
test('optionsFromConfig uses config without modification', () => {
|
||||
const get = key => {
|
||||
const suffixes = [
|
||||
'token',
|
||||
];
|
||||
const value = suffixes.find(suffix => {
|
||||
return `xpack.notifications.slack.${suffix}` === key;
|
||||
});
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error(`Unknown config key used ${key}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
expect(optionsFromConfig({ get })).toEqual({
|
||||
token: 'token',
|
||||
});
|
||||
});
|
||||
|
||||
test('defaultsFromConfig uses config without modification', () => {
|
||||
const get = key => {
|
||||
const suffixes = [
|
||||
'channel',
|
||||
'as_user',
|
||||
'icon_emoji',
|
||||
'icon_url',
|
||||
'link_names',
|
||||
'mrkdwn',
|
||||
'unfurl_links',
|
||||
'unfurl_media',
|
||||
'username',
|
||||
];
|
||||
const value = suffixes.find(suffix => {
|
||||
return `xpack.notifications.slack.defaults.${suffix}` === key;
|
||||
});
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error(`Unknown config key used ${key}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
expect(defaultsFromConfig({ get })).toEqual({
|
||||
channel: 'channel',
|
||||
as_user: 'as_user',
|
||||
icon_emoji: 'icon_emoji',
|
||||
icon_url: 'icon_url',
|
||||
link_names: 'link_names',
|
||||
mrkdwn: 'mrkdwn',
|
||||
unfurl_links: 'unfurl_links',
|
||||
unfurl_media: 'unfurl_media',
|
||||
username: 'username',
|
||||
});
|
||||
});
|
||||
|
||||
test('createSlackAction', async () => {
|
||||
const config = { };
|
||||
const server = { config: jest.fn().mockReturnValue(config) };
|
||||
const _options = jest.fn().mockReturnValue({ options: true });
|
||||
const defaults = { defaults: true };
|
||||
const _defaults = jest.fn().mockReturnValue(defaults);
|
||||
|
||||
const action = createSlackAction(server, { _options, _defaults });
|
||||
|
||||
expect(action instanceof SlackAction).toBe(true);
|
||||
expect(action.defaults).toBe(defaults);
|
||||
|
||||
expect(server.config).toHaveBeenCalledTimes(1);
|
||||
expect(server.config).toHaveBeenCalledWith();
|
||||
expect(_options).toHaveBeenCalledTimes(1);
|
||||
expect(_options).toHaveBeenCalledWith(config);
|
||||
expect(_defaults).toHaveBeenCalledTimes(1);
|
||||
expect(_defaults).toHaveBeenCalledWith(config);
|
||||
});
|
||||
|
||||
});
|
7
x-pack/plugins/notifications/server/slack/index.js
Normal file
7
x-pack/plugins/notifications/server/slack/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { createSlackAction } from './create_slack_action';
|
116
x-pack/plugins/notifications/server/slack/slack_action.js
Normal file
116
x-pack/plugins/notifications/server/slack/slack_action.js
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { WebClient } from '@slack/client';
|
||||
|
||||
import { Action, ActionResult } from '../';
|
||||
|
||||
export const SLACK_ACTION_ID = 'xpack-notifications-slack';
|
||||
|
||||
/**
|
||||
* Create a new Slack {@code WebClient}.
|
||||
*
|
||||
* Currently the only option expected is {@code token}.
|
||||
*
|
||||
* @param {Object} options Slack API options.
|
||||
* @returns {WebClient} Always.
|
||||
*/
|
||||
export function webClientCreator(options) {
|
||||
return new WebClient(options.token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Slack Action enables generic sending of Slack messages.
|
||||
*/
|
||||
export class SlackAction extends Action {
|
||||
|
||||
/**
|
||||
* Create a new Action capable of sending Slack messages.
|
||||
*
|
||||
* @param {Object} server Kibana server object.
|
||||
* @param {Object} options Configuration options for the Slack WebClient. Currently only expect "token" field.
|
||||
* @param {Object} defaults Default fields used when sending messages.
|
||||
* @param {Function} _webClientCreator Exposed for tests.
|
||||
*/
|
||||
constructor({ server, options, defaults = { }, _webClientCreator = webClientCreator }) {
|
||||
super({ server, id: SLACK_ACTION_ID, name: 'Slack' });
|
||||
|
||||
this.client = _webClientCreator(options);
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
getMissingFields(data) {
|
||||
const missingFields = [];
|
||||
|
||||
if (!Boolean(this.defaults.channel) && !Boolean(data.channel)) {
|
||||
missingFields.push({
|
||||
field: 'channel',
|
||||
name: 'Channel',
|
||||
type: 'text',
|
||||
});
|
||||
}
|
||||
|
||||
if (!Boolean(data.subject)) {
|
||||
missingFields.push({
|
||||
field: 'subject',
|
||||
name: 'Message',
|
||||
type: 'markdown',
|
||||
});
|
||||
}
|
||||
|
||||
return missingFields;
|
||||
}
|
||||
|
||||
async doPerformHealthCheck() {
|
||||
const response = await this.client.api.test();
|
||||
|
||||
if (response.ok) {
|
||||
return new ActionResult({
|
||||
message: `Slack action configuration has been verified.`,
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
return new ActionResult({
|
||||
message: `Slack action configuration could not be verified.`,
|
||||
response,
|
||||
error: response.error || { message: 'Unknown Error' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the message based on whether or not a {@code markdown} body was supplied.
|
||||
*/
|
||||
renderMessage({ subject, markdown }) {
|
||||
const attachments = [];
|
||||
|
||||
if (markdown) {
|
||||
attachments.push({ text: markdown });
|
||||
}
|
||||
|
||||
return { text: subject, attachments };
|
||||
}
|
||||
|
||||
async doPerformAction({ subject, markdown, channel }) {
|
||||
// NOTE: When we want to support files, then we should look into using client.files.upload({ ... })
|
||||
// without _also_ sending chat message because the file upload endpoint supports chat behavior
|
||||
// in addition to files, but the reverse is not true.
|
||||
const slackChannel = channel || this.defaults.channel;
|
||||
|
||||
const response = await this.client.chat.postMessage({
|
||||
...this.defaults,
|
||||
...this.renderMessage({ subject, markdown }),
|
||||
channel: slackChannel,
|
||||
});
|
||||
|
||||
return new ActionResult({
|
||||
message: `Posted Slack message to channel '${slackChannel}'.`,
|
||||
response,
|
||||
error: response.error,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
277
x-pack/plugins/notifications/server/slack/slack_action.test.js
Normal file
277
x-pack/plugins/notifications/server/slack/slack_action.test.js
Normal file
|
@ -0,0 +1,277 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { WebClient } from '@slack/client';
|
||||
import { ActionResult } from '../';
|
||||
import {
|
||||
SLACK_ACTION_ID,
|
||||
SlackAction,
|
||||
webClientCreator,
|
||||
} from './slack_action';
|
||||
|
||||
describe('SlackAction', () => {
|
||||
|
||||
const server = { };
|
||||
const options = { options: true };
|
||||
const defaults = { defaults: true };
|
||||
const client = {
|
||||
api: {
|
||||
// see beforeEach
|
||||
},
|
||||
chat: {
|
||||
// see beforeEach
|
||||
}
|
||||
};
|
||||
let _webClientCreator;
|
||||
|
||||
let action;
|
||||
|
||||
beforeEach(() => {
|
||||
client.api.test = jest.fn();
|
||||
client.chat.postMessage = jest.fn();
|
||||
_webClientCreator = jest.fn().mockReturnValue(client);
|
||||
|
||||
action = new SlackAction({ server, options, defaults, _webClientCreator });
|
||||
});
|
||||
|
||||
test('webClientCreator creates a WebClient', () => {
|
||||
expect(webClientCreator('faketoken') instanceof WebClient).toBe(true);
|
||||
});
|
||||
|
||||
test('id and name to be from constructor', () => {
|
||||
expect(action.getId()).toBe(SLACK_ACTION_ID);
|
||||
expect(action.getName()).toBe('Slack');
|
||||
expect(action.client).toBe(client);
|
||||
|
||||
expect(_webClientCreator).toHaveBeenCalledTimes(1);
|
||||
expect(_webClientCreator).toHaveBeenCalledWith(options);
|
||||
});
|
||||
|
||||
describe('getMissingFields', () => {
|
||||
|
||||
test('returns missing fields', () => {
|
||||
const channel = { field: 'channel', name: 'Channel', type: 'text' };
|
||||
const subject = { field: 'subject', name: 'Message', type: 'markdown' };
|
||||
|
||||
const missing = [
|
||||
{ defaults: { }, notification: { }, missing: [ channel, subject, ], },
|
||||
{ defaults: { }, notification: { channel: '#kibana', }, missing: [ subject, ], },
|
||||
{ defaults: { channel: '#kibana', }, notification: { }, missing: [ subject, ], },
|
||||
{ defaults: { }, notification: { subject: 'subject', }, missing: [ channel, ], },
|
||||
];
|
||||
|
||||
missing.forEach(check => {
|
||||
const newDefaultsAction = new SlackAction({ server, options, defaults: check.defaults, _webClientCreator });
|
||||
|
||||
expect(newDefaultsAction.getMissingFields(check.notification)).toEqual(check.missing);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns [] when all fields exist', () => {
|
||||
const exists = [
|
||||
{ defaults: { }, notification: { channel: '#kibana', subject: 'subject', }, },
|
||||
{ defaults: { channel: '#kibana', }, notification: { subject: 'subject', }, },
|
||||
];
|
||||
|
||||
exists.forEach(check => {
|
||||
const newDefaultsAction = new SlackAction({ server, options, defaults: check.defaults, _webClientCreator });
|
||||
|
||||
expect(newDefaultsAction.getMissingFields(check.notification)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('doPerformHealthCheck', () => {
|
||||
|
||||
test('rethrows Error for failure', async () => {
|
||||
const error = new Error('TEST - expected');
|
||||
|
||||
client.api.test.mockRejectedValue(error);
|
||||
|
||||
await expect(action.doPerformHealthCheck())
|
||||
.rejects
|
||||
.toThrow(error);
|
||||
|
||||
expect(client.api.test).toHaveBeenCalledTimes(1);
|
||||
expect(client.api.test).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('returns ActionResult if not ok with error', async () => {
|
||||
const response = { ok: false, error: { expected: true } };
|
||||
|
||||
client.api.test.mockResolvedValue(response);
|
||||
|
||||
const result = await action.doPerformHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage()).toMatch('Slack action configuration could not be verified.');
|
||||
expect(result.getResponse()).toBe(response);
|
||||
expect(result.getError()).toBe(response.error);
|
||||
|
||||
expect(client.api.test).toHaveBeenCalledTimes(1);
|
||||
expect(client.api.test).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('returns ActionResult if not ok with default error', async () => {
|
||||
const response = { ok: false };
|
||||
|
||||
client.api.test.mockResolvedValue(response);
|
||||
|
||||
const result = await action.doPerformHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage()).toMatch('Slack action configuration could not be verified.');
|
||||
expect(result.getResponse()).toBe(response);
|
||||
expect(result.getError()).toEqual({ message: 'Unknown Error' });
|
||||
|
||||
expect(client.api.test).toHaveBeenCalledTimes(1);
|
||||
expect(client.api.test).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('returns ActionResult for success', async () => {
|
||||
const response = { ok: true };
|
||||
|
||||
client.api.test.mockResolvedValue(response);
|
||||
|
||||
const result = await action.doPerformHealthCheck();
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch('Slack action configuration has been verified.');
|
||||
expect(result.getResponse()).toBe(response);
|
||||
|
||||
expect(client.api.test).toHaveBeenCalledTimes(1);
|
||||
expect(client.api.test).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('renderMessage', () => {
|
||||
|
||||
test('does not contain attachments', () => {
|
||||
const message = { subject: 'subject' };
|
||||
const response = action.renderMessage(message);
|
||||
|
||||
expect(response).toMatchObject({
|
||||
text: message.subject,
|
||||
attachments: [ ]
|
||||
});
|
||||
});
|
||||
|
||||
test('contains attachments', () => {
|
||||
const message = { subject: 'subject', markdown: 'markdown' };
|
||||
const response = action.renderMessage(message);
|
||||
|
||||
expect(response).toMatchObject({
|
||||
text: message.subject,
|
||||
attachments: [
|
||||
{
|
||||
text: message.markdown
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('doPerformAction', () => {
|
||||
const message = { channel: '#kibana', subject: 'subject', markdown: 'body', };
|
||||
|
||||
test('rethrows Error for failure', async () => {
|
||||
const error = new Error('TEST - expected');
|
||||
|
||||
client.chat.postMessage.mockRejectedValue(error);
|
||||
|
||||
await expect(action.doPerformAction(message))
|
||||
.rejects
|
||||
.toThrow(error);
|
||||
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(client.chat.postMessage).toHaveBeenCalledWith({
|
||||
...defaults,
|
||||
...action.renderMessage(message),
|
||||
channel: message.channel,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns ActionResult for failure without Error', async () => {
|
||||
const response = { fake: true, error: { expected: true } };
|
||||
|
||||
client.chat.postMessage.mockResolvedValue(response);
|
||||
|
||||
const result = await action.doPerformAction(message);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.getMessage()).toMatch(`Posted Slack message to channel '${message.channel}'.`);
|
||||
expect(result.getResponse()).toBe(response);
|
||||
expect(result.getError()).toBe(response.error);
|
||||
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(client.chat.postMessage).toHaveBeenCalledWith({
|
||||
...defaults,
|
||||
...action.renderMessage(message),
|
||||
channel: message.channel,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('returns ActionResult for success', async () => {
|
||||
const response = { fake: true };
|
||||
|
||||
client.chat.postMessage.mockResolvedValue(response);
|
||||
|
||||
const result = await action.doPerformAction(message);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch(`Posted Slack message to channel '${message.channel}'.`);
|
||||
expect(result.getResponse()).toBe(response);
|
||||
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(client.chat.postMessage).toHaveBeenCalledWith({
|
||||
...defaults,
|
||||
...action.renderMessage(message),
|
||||
channel: message.channel,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns ActionResult for success with default channel', async () => {
|
||||
const response = { fake: false };
|
||||
|
||||
client.chat.postMessage.mockResolvedValue(response);
|
||||
|
||||
const channelDefaults = {
|
||||
...defaults,
|
||||
channel: '#kibana',
|
||||
};
|
||||
const noChannelMessage = {
|
||||
...message,
|
||||
channel: undefined,
|
||||
};
|
||||
const newDefaultsAction = new SlackAction({ server, options, defaults: channelDefaults, _webClientCreator });
|
||||
|
||||
const result = await newDefaultsAction.doPerformAction(noChannelMessage);
|
||||
|
||||
expect(result instanceof ActionResult).toBe(true);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.getMessage()).toMatch(`Posted Slack message to channel '${channelDefaults.channel}'.`);
|
||||
expect(result.getResponse()).toBe(response);
|
||||
|
||||
expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
|
||||
expect(client.chat.postMessage).toHaveBeenCalledWith({
|
||||
...defaults,
|
||||
...action.renderMessage(noChannelMessage),
|
||||
channel: channelDefaults.channel,
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -39,6 +39,9 @@ export class XPackInfoLicense {
|
|||
|
||||
/**
|
||||
* Returns license expiration date in ms.
|
||||
*
|
||||
* Note: A basic license created after 6.3 will have no expiration, thus returning undefined.
|
||||
*
|
||||
* @returns {number|undefined}
|
||||
*/
|
||||
getExpiryDateInMillis() {
|
||||
|
@ -47,7 +50,7 @@ export class XPackInfoLicense {
|
|||
|
||||
/**
|
||||
* Checks if the license is represented in a specified license list.
|
||||
* @param candidateLicenses List of the licenses to check against.
|
||||
* @param {String} candidateLicenses List of the licenses to check against.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isOneOf(candidateLicenses) {
|
||||
|
@ -65,4 +68,46 @@ export class XPackInfoLicense {
|
|||
getType() {
|
||||
return get(this._getRawLicense(), 'type');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns mode of the license (basic, gold etc.). This is the "effective" type of the license.
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
getMode() {
|
||||
return get(this._getRawLicense(), 'mode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current license is active and the supplied {@code type}.
|
||||
*
|
||||
* @param {Function} typeChecker The license type checker.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isActiveLicense(typeChecker) {
|
||||
const license = this._getRawLicense();
|
||||
|
||||
return get(license, 'status') === 'active' && typeChecker(get(license, 'mode'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the license is an active, basic license.
|
||||
*
|
||||
* Note: This also verifies that the license is active. Therefore it is not safe to assume that !isBasic() === isNotBasic().
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isBasic() {
|
||||
return this.isActiveLicense(mode => mode === 'basic');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the license is an active, non-basic license (e.g., standard, gold, platinum, or trial).
|
||||
*
|
||||
* Note: This also verifies that the license is active. Therefore it is not safe to assume that !isBasic() === isNotBasic().
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isNotBasic() {
|
||||
return this.isActiveLicense(mode => mode !== 'basic');
|
||||
}
|
||||
}
|
||||
|
|
183
x-pack/plugins/xpack_main/server/lib/xpack_info_license.test.js
Normal file
183
x-pack/plugins/xpack_main/server/lib/xpack_info_license.test.js
Normal file
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { XPackInfoLicense } from './xpack_info_license';
|
||||
|
||||
function getXPackInfoLicense(getRawLicense) {
|
||||
return new XPackInfoLicense(getRawLicense);
|
||||
}
|
||||
|
||||
describe('XPackInfoLicense', () => {
|
||||
|
||||
const xpackInfoLicenseUndefined = getXPackInfoLicense(() => { });
|
||||
let xpackInfoLicense;
|
||||
let getRawLicense;
|
||||
|
||||
beforeEach(() => {
|
||||
getRawLicense = jest.fn();
|
||||
xpackInfoLicense = getXPackInfoLicense(getRawLicense);
|
||||
});
|
||||
|
||||
test('getUid returns uid field', () => {
|
||||
const uid = 'abc123';
|
||||
|
||||
getRawLicense.mockReturnValue({ uid });
|
||||
|
||||
expect(xpackInfoLicense.getUid()).toBe(uid);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(xpackInfoLicenseUndefined.getUid()).toBe(undefined);
|
||||
});
|
||||
|
||||
test('isActive returns true if status is active', () => {
|
||||
getRawLicense.mockReturnValue({ status: 'active' });
|
||||
|
||||
expect(xpackInfoLicense.isActive()).toBe(true);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('isActive returns false if status is not active', () => {
|
||||
getRawLicense.mockReturnValue({ status: 'aCtIvE' }); // needs to match exactly
|
||||
|
||||
expect(xpackInfoLicense.isActive()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(xpackInfoLicenseUndefined.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
test('getExpiryDateInMillis returns expiry_date_in_millis', () => {
|
||||
getRawLicense.mockReturnValue({ expiry_date_in_millis: 123 });
|
||||
|
||||
expect(xpackInfoLicense.getExpiryDateInMillis()).toBe(123);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(xpackInfoLicenseUndefined.getExpiryDateInMillis()).toBe(undefined);
|
||||
});
|
||||
|
||||
test('isOneOf returns true of the mode includes one of the types', () => {
|
||||
getRawLicense.mockReturnValue({ mode: 'platinum' });
|
||||
|
||||
expect(xpackInfoLicense.isOneOf('platinum')).toBe(true);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(xpackInfoLicense.isOneOf([ 'platinum' ])).toBe(true);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(2);
|
||||
expect(xpackInfoLicense.isOneOf([ 'gold', 'platinum' ])).toBe(true);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(3);
|
||||
expect(xpackInfoLicense.isOneOf([ 'platinum', 'gold' ])).toBe(true);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(4);
|
||||
expect(xpackInfoLicense.isOneOf([ 'basic', 'gold' ])).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(5);
|
||||
expect(xpackInfoLicense.isOneOf([ 'basic' ])).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(6);
|
||||
|
||||
expect(xpackInfoLicenseUndefined.isOneOf([ 'platinum', 'gold' ])).toBe(false);
|
||||
});
|
||||
|
||||
test('getType returns the type', () => {
|
||||
getRawLicense.mockReturnValue({ type: 'basic' });
|
||||
|
||||
expect(xpackInfoLicense.getType()).toBe('basic');
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
getRawLicense.mockReturnValue({ type: 'gold' });
|
||||
|
||||
expect(xpackInfoLicense.getType()).toBe('gold');
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(xpackInfoLicenseUndefined.getType()).toBe(undefined);
|
||||
});
|
||||
|
||||
test('getMode returns the mode', () => {
|
||||
getRawLicense.mockReturnValue({ mode: 'basic' });
|
||||
|
||||
expect(xpackInfoLicense.getMode()).toBe('basic');
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
getRawLicense.mockReturnValue({ mode: 'gold' });
|
||||
|
||||
expect(xpackInfoLicense.getMode()).toBe('gold');
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(xpackInfoLicenseUndefined.getMode()).toBe(undefined);
|
||||
});
|
||||
|
||||
test('isActiveLicense returns the true if active and typeChecker matches', () => {
|
||||
const expectAbc123 = type => type === 'abc123';
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'active', mode: 'abc123' });
|
||||
|
||||
expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(true);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'abc123' });
|
||||
|
||||
expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(2);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'NOTabc123' });
|
||||
|
||||
expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(3);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'active', mode: 'NOTabc123' });
|
||||
|
||||
expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(4);
|
||||
|
||||
expect(xpackInfoLicenseUndefined.isActive(expectAbc123)).toBe(false);
|
||||
});
|
||||
|
||||
test('isBasic returns the true if active and basic', () => {
|
||||
getRawLicense.mockReturnValue({ status: 'active', mode: 'basic' });
|
||||
|
||||
expect(xpackInfoLicense.isBasic()).toBe(true);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'gold' });
|
||||
|
||||
expect(xpackInfoLicense.isBasic()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(2);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'trial' });
|
||||
|
||||
expect(xpackInfoLicense.isBasic()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(3);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'active', mode: 'platinum' });
|
||||
|
||||
expect(xpackInfoLicense.isBasic()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(4);
|
||||
|
||||
expect(xpackInfoLicenseUndefined.isBasic()).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
test('isNotBasic returns the true if active and not basic', () => {
|
||||
getRawLicense.mockReturnValue({ status: 'active', mode: 'platinum' });
|
||||
|
||||
expect(xpackInfoLicense.isNotBasic()).toBe(true);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(1);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'gold' });
|
||||
|
||||
expect(xpackInfoLicense.isNotBasic()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(2);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'trial' });
|
||||
|
||||
expect(xpackInfoLicense.isNotBasic()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(3);
|
||||
|
||||
getRawLicense.mockReturnValue({ status: 'active', mode: 'basic' });
|
||||
|
||||
expect(xpackInfoLicense.isNotBasic()).toBe(false);
|
||||
expect(getRawLicense).toHaveBeenCalledTimes(4);
|
||||
|
||||
expect(xpackInfoLicenseUndefined.isNotBasic()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
346
x-pack/yarn.lock
346
x-pack/yarn.lock
|
@ -70,20 +70,120 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@sindresorhus/is@^0.7.0":
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
|
||||
|
||||
"@sinonjs/formatio@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-2.0.0.tgz#84db7e9eb5531df18a8c5e0bfb6e449e55e654b2"
|
||||
dependencies:
|
||||
samsam "1.3.0"
|
||||
|
||||
"@slack/client@^4.2.2":
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@slack/client/-/client-4.2.2.tgz#f997f39780bbff9c2128816e8377230a5f6bd0d5"
|
||||
dependencies:
|
||||
"@types/delay" "^2.0.1"
|
||||
"@types/form-data" "^2.2.1"
|
||||
"@types/got" "^7.1.7"
|
||||
"@types/is-stream" "^1.1.0"
|
||||
"@types/loglevel" "^1.5.3"
|
||||
"@types/node" "^9.4.7"
|
||||
"@types/p-cancelable" "^0.3.0"
|
||||
"@types/p-queue" "^2.3.1"
|
||||
"@types/p-retry" "^1.0.1"
|
||||
"@types/retry" "^0.10.2"
|
||||
"@types/url-join" "^0.8.2"
|
||||
"@types/ws" "^4.0.1"
|
||||
delay "^2.0.0"
|
||||
eventemitter3 "^3.0.0"
|
||||
finity "^0.5.4"
|
||||
form-data "^2.3.1"
|
||||
got "^8.0.3"
|
||||
is-stream "^1.1.0"
|
||||
loglevel "^1.6.1"
|
||||
object.entries "^1.0.4"
|
||||
object.getownpropertydescriptors "^2.0.3"
|
||||
object.values "^1.0.4"
|
||||
p-cancelable "^0.3.0"
|
||||
p-queue "^2.3.0"
|
||||
p-retry "^1.0.0"
|
||||
retry "^0.10.1"
|
||||
url-join "^4.0.0"
|
||||
ws "^4.1.0"
|
||||
|
||||
"@types/delay@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901"
|
||||
|
||||
"@types/events@*":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
|
||||
|
||||
"@types/form-data@^2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/got@^7.1.7":
|
||||
version "7.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/got/-/got-7.1.8.tgz#c5f421b25770689bf8948b1241f710d71a00d7dd"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/is-stream@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/jest@^22.2.3":
|
||||
version "22.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.2.3.tgz#0157c0316dc3722c43a7b71de3fdf3acbccef10d"
|
||||
|
||||
"@types/loglevel@^1.5.3":
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/loglevel/-/loglevel-1.5.3.tgz#adfce55383edc5998a2170ad581b3e23d6adb5b8"
|
||||
|
||||
"@types/node@*":
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.3.0.tgz#3a129cda7c4e5df2409702626892cb4b96546dd5"
|
||||
|
||||
"@types/node@^9.4.7":
|
||||
version "9.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.18.tgz#092e13ef64c47e986802c9c45a61c1454813b31d"
|
||||
|
||||
"@types/p-cancelable@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/p-cancelable/-/p-cancelable-0.3.0.tgz#3e4fcc54a3dfd81d0f5b93546bb68d0df50553bb"
|
||||
|
||||
"@types/p-queue@^2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/p-queue/-/p-queue-2.3.1.tgz#2fb251e46e884e31c4bd1bf58f0e188972353ff4"
|
||||
|
||||
"@types/p-retry@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/p-retry/-/p-retry-1.0.1.tgz#2302bc3da425014208c8a9b68293d37325124785"
|
||||
dependencies:
|
||||
"@types/retry" "*"
|
||||
|
||||
"@types/retry@*", "@types/retry@^0.10.2":
|
||||
version "0.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.10.2.tgz#bd1740c4ad51966609b058803ee6874577848b37"
|
||||
|
||||
"@types/url-join@^0.8.2":
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/url-join/-/url-join-0.8.2.tgz#1181ecbe1d97b7034e0ea1e35e62e86cc26b422d"
|
||||
|
||||
"@types/ws@^4.0.1":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-4.0.2.tgz#b29037627dd7ba31ec49a4f1584840422efb856f"
|
||||
dependencies:
|
||||
"@types/events" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
abab@^1.0.3, abab@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
|
||||
|
@ -402,6 +502,10 @@ astral-regex@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
|
||||
|
||||
async-limiter@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
|
||||
|
||||
async@2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.4.0.tgz#4990200f18ea5b837c2cc4f8c031a6985c385611"
|
||||
|
@ -1110,6 +1214,18 @@ cache-base@^1.0.1:
|
|||
union-value "^1.0.0"
|
||||
unset-value "^1.0.0"
|
||||
|
||||
cacheable-request@^2.1.1:
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d"
|
||||
dependencies:
|
||||
clone-response "1.0.2"
|
||||
get-stream "3.0.0"
|
||||
http-cache-semantics "3.8.1"
|
||||
keyv "3.0.0"
|
||||
lowercase-keys "1.0.0"
|
||||
normalize-url "2.0.1"
|
||||
responselike "1.0.2"
|
||||
|
||||
call@3.x.x:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/call/-/call-3.0.4.tgz#e380f2f2a491330aa79085355f8be080877d559e"
|
||||
|
@ -1298,6 +1414,12 @@ clone-buffer@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
|
||||
|
||||
clone-response@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
|
||||
dependencies:
|
||||
mimic-response "^1.0.0"
|
||||
|
||||
clone-stats@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
|
||||
|
@ -1744,6 +1866,12 @@ decode-uri-component@^0.2.0:
|
|||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||
|
||||
decompress-response@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
|
||||
dependencies:
|
||||
mimic-response "^1.0.0"
|
||||
|
||||
dedent@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
|
||||
|
@ -1821,6 +1949,12 @@ del@^2.2.2:
|
|||
pinkie-promise "^2.0.0"
|
||||
rimraf "^2.2.8"
|
||||
|
||||
delay@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delay/-/delay-2.0.0.tgz#9112eadc03e4ec7e00297337896f273bbd91fae5"
|
||||
dependencies:
|
||||
p-defer "^1.0.0"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
|
@ -1942,6 +2076,10 @@ duplexer2@0.0.2, duplexer2@~0.0.2:
|
|||
dependencies:
|
||||
readable-stream "~1.1.9"
|
||||
|
||||
duplexer3@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
|
||||
|
||||
duplexify@^3.5.3:
|
||||
version "3.5.4"
|
||||
resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.4.tgz#4bb46c1796eabebeec4ca9a2e66b808cb7a3d8b4"
|
||||
|
@ -2159,6 +2297,10 @@ esutils@~1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.0.0.tgz#8151d358e20c8acc7fb745e7472c0025fe496570"
|
||||
|
||||
eventemitter3@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
|
||||
|
||||
exec-sh@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38"
|
||||
|
@ -2448,6 +2590,10 @@ fined@^1.0.1:
|
|||
object.pick "^1.2.0"
|
||||
parse-filepath "^1.0.1"
|
||||
|
||||
finity@^0.5.4:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/finity/-/finity-0.5.4.tgz#f2a8a9198e8286467328ec32c8bfcc19a2229c11"
|
||||
|
||||
first-chunk-stream@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz#59bfb50cd905f60d7c394cd3d9acaab4e6ad934e"
|
||||
|
@ -2551,6 +2697,13 @@ fragment-cache@^0.2.1:
|
|||
dependencies:
|
||||
map-cache "^0.2.2"
|
||||
|
||||
from2@^2.1.1:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
|
||||
dependencies:
|
||||
inherits "^2.0.1"
|
||||
readable-stream "^2.0.0"
|
||||
|
||||
from@^0.1.3:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
|
||||
|
@ -2641,7 +2794,7 @@ get-stdin@^4.0.1:
|
|||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
||||
|
||||
get-stream@^3.0.0:
|
||||
get-stream@3.0.0, get-stream@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
|
||||
|
||||
|
@ -2859,6 +3012,28 @@ good-listener@^1.2.2:
|
|||
dependencies:
|
||||
delegate "^3.1.2"
|
||||
|
||||
got@^8.0.3:
|
||||
version "8.3.1"
|
||||
resolved "https://registry.yarnpkg.com/got/-/got-8.3.1.tgz#093324403d4d955f5a16a7a8d39955d055ae10ed"
|
||||
dependencies:
|
||||
"@sindresorhus/is" "^0.7.0"
|
||||
cacheable-request "^2.1.1"
|
||||
decompress-response "^3.3.0"
|
||||
duplexer3 "^0.1.4"
|
||||
get-stream "^3.0.0"
|
||||
into-stream "^3.1.0"
|
||||
is-retry-allowed "^1.1.0"
|
||||
isurl "^1.0.0-alpha5"
|
||||
lowercase-keys "^1.0.0"
|
||||
mimic-response "^1.0.0"
|
||||
p-cancelable "^0.4.0"
|
||||
p-timeout "^2.0.1"
|
||||
pify "^3.0.0"
|
||||
safe-buffer "^5.1.1"
|
||||
timed-out "^4.0.1"
|
||||
url-parse-lax "^3.0.0"
|
||||
url-to-options "^1.0.1"
|
||||
|
||||
graceful-fs@^3.0.0:
|
||||
version "3.0.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-3.0.11.tgz#7613c778a1afea62f25c630a086d7f3acbbdd818"
|
||||
|
@ -3100,10 +3275,20 @@ has-gulplog@^0.1.0:
|
|||
dependencies:
|
||||
sparkles "^1.0.0"
|
||||
|
||||
has-symbol-support-x@^1.4.1:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455"
|
||||
|
||||
has-symbols@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
|
||||
|
||||
has-to-string-tag-x@^1.2.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d"
|
||||
dependencies:
|
||||
has-symbol-support-x "^1.4.1"
|
||||
|
||||
has-unicode@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
|
||||
|
@ -3247,6 +3432,10 @@ htmlparser2@^3.9.1:
|
|||
inherits "^2.0.1"
|
||||
readable-stream "^2.0.2"
|
||||
|
||||
http-cache-semantics@3.8.1:
|
||||
version "3.8.1"
|
||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
|
||||
|
||||
http-errors@~1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.4.0.tgz#6c0242dea6b3df7afda153c71089b31c6e82aabf"
|
||||
|
@ -3361,6 +3550,13 @@ interpret@^1.0.0:
|
|||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
|
||||
|
||||
into-stream@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6"
|
||||
dependencies:
|
||||
from2 "^2.1.1"
|
||||
p-is-promise "^1.1.0"
|
||||
|
||||
invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
|
||||
|
@ -3534,7 +3730,7 @@ is-number@^3.0.0:
|
|||
dependencies:
|
||||
kind-of "^3.0.2"
|
||||
|
||||
is-object@~1.0.1:
|
||||
is-object@^1.0.1, is-object@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
|
||||
|
||||
|
@ -3560,6 +3756,10 @@ is-path-inside@^1.0.0:
|
|||
dependencies:
|
||||
path-is-inside "^1.0.1"
|
||||
|
||||
is-plain-obj@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
|
||||
|
||||
is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
|
||||
|
@ -3590,6 +3790,10 @@ is-relative@^1.0.0:
|
|||
dependencies:
|
||||
is-unc-path "^1.0.0"
|
||||
|
||||
is-retry-allowed@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
|
||||
|
||||
is-stream@^1.0.1, is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
|
@ -3728,6 +3932,13 @@ istanbul-reports@^1.1.3:
|
|||
dependencies:
|
||||
handlebars "^4.0.3"
|
||||
|
||||
isurl@^1.0.0-alpha5:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67"
|
||||
dependencies:
|
||||
has-to-string-tag-x "^1.2.0"
|
||||
is-object "^1.0.1"
|
||||
|
||||
items@2.x.x:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/items/-/items-2.1.1.tgz#8bd16d9c83b19529de5aea321acaada78364a198"
|
||||
|
@ -4105,6 +4316,10 @@ jsesc@~0.5.0:
|
|||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
|
||||
|
||||
json-buffer@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
|
||||
|
||||
json-schema-traverse@^0.3.0:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
|
||||
|
@ -4156,6 +4371,12 @@ keymirror@^0.1.1:
|
|||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/keymirror/-/keymirror-0.1.1.tgz#918889ea13f8d0a42e7c557250eee713adc95c35"
|
||||
|
||||
keyv@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373"
|
||||
dependencies:
|
||||
json-buffer "3.0.0"
|
||||
|
||||
kilt@2.x.x:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/kilt/-/kilt-2.0.2.tgz#04d7183c298a1232efddf7ddca5959a8f6301e20"
|
||||
|
@ -4512,6 +4733,10 @@ lodash@~1.0.1:
|
|||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551"
|
||||
|
||||
loglevel@^1.6.1:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa"
|
||||
|
||||
lolex@^2.2.0, lolex@^2.3.2:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.6.0.tgz#cf9166f3c9dece3cdeb5d6b01fce50f14a1203e3"
|
||||
|
@ -4533,6 +4758,14 @@ loud-rejection@^1.0.0:
|
|||
currently-unhandled "^0.4.1"
|
||||
signal-exit "^3.0.0"
|
||||
|
||||
lowercase-keys@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
|
||||
|
||||
lowercase-keys@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
|
||||
|
||||
lowlight@~1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.9.1.tgz#ed7c3dffc36f8c1f263735c0fe0c907847c11250"
|
||||
|
@ -4677,6 +4910,10 @@ mimic-fn@^1.0.0:
|
|||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
|
||||
|
||||
mimic-response@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e"
|
||||
|
||||
mimos@3.x.x:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/mimos/-/mimos-3.0.3.tgz#b9109072ad378c2b72f6a0101c43ddfb2b36641f"
|
||||
|
@ -4939,6 +5176,10 @@ node-pre-gyp@^0.6.39:
|
|||
tar "^2.2.1"
|
||||
tar-pack "^3.4.0"
|
||||
|
||||
nodemailer@^4.6.4:
|
||||
version "4.6.4"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.6.4.tgz#f0d72d0c6a6ec5f4369fa8f4bf5127a31baa2014"
|
||||
|
||||
nomnom@~1.6.2:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.6.2.tgz#84a66a260174408fc5b77a18f888eccc44fb6971"
|
||||
|
@ -4968,6 +5209,14 @@ normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1:
|
|||
dependencies:
|
||||
remove-trailing-separator "^1.0.1"
|
||||
|
||||
normalize-url@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6"
|
||||
dependencies:
|
||||
prepend-http "^2.0.0"
|
||||
query-string "^5.0.1"
|
||||
sort-keys "^2.0.0"
|
||||
|
||||
now-and-later@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.0.tgz#bc61cbb456d79cb32207ce47ca05136ff2e7d6ee"
|
||||
|
@ -5207,10 +5456,26 @@ osenv@^0.1.4:
|
|||
os-homedir "^1.0.0"
|
||||
os-tmpdir "^1.0.0"
|
||||
|
||||
p-cancelable@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
|
||||
|
||||
p-cancelable@^0.4.0:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0"
|
||||
|
||||
p-defer@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
|
||||
|
||||
p-finally@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
|
||||
|
||||
p-is-promise@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
|
||||
|
||||
p-limit@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
|
||||
|
@ -5223,6 +5488,22 @@ p-locate@^2.0.0:
|
|||
dependencies:
|
||||
p-limit "^1.1.0"
|
||||
|
||||
p-queue@^2.3.0:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-2.4.2.tgz#03609826682b743be9a22dba25051bd46724fc34"
|
||||
|
||||
p-retry@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-1.0.0.tgz#3927332a4b7d70269b535515117fc547da1a6968"
|
||||
dependencies:
|
||||
retry "^0.10.0"
|
||||
|
||||
p-timeout@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038"
|
||||
dependencies:
|
||||
p-finally "^1.0.0"
|
||||
|
||||
p-try@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
|
||||
|
@ -5383,6 +5664,10 @@ pify@^2.0.0:
|
|||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||
|
||||
pify@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
|
||||
|
||||
pinkie-promise@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
|
||||
|
@ -5483,6 +5768,10 @@ prelude-ls@~1.1.2:
|
|||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||
|
||||
prepend-http@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
|
||||
preserve@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
|
||||
|
@ -5620,6 +5909,14 @@ qs@~6.4.0:
|
|||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
|
||||
|
||||
query-string@^5.0.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
|
||||
dependencies:
|
||||
decode-uri-component "^0.2.0"
|
||||
object-assign "^4.1.0"
|
||||
strict-uri-encode "^1.0.0"
|
||||
|
||||
quote-stream@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/quote-stream/-/quote-stream-1.0.2.tgz#84963f8c9c26b942e153feeb53aae74652b7e0b2"
|
||||
|
@ -6272,6 +6569,12 @@ resolve@^1.1.5, resolve@^1.1.6, resolve@^1.1.7:
|
|||
dependencies:
|
||||
path-parse "^1.0.5"
|
||||
|
||||
responselike@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
|
||||
dependencies:
|
||||
lowercase-keys "^1.0.0"
|
||||
|
||||
restore-cursor@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
|
||||
|
@ -6289,6 +6592,10 @@ ret@~0.1.10:
|
|||
version "0.1.15"
|
||||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||
|
||||
retry@^0.10.0, retry@^0.10.1:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
|
||||
|
||||
right-align@^0.1.1:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
|
||||
|
@ -6546,6 +6853,12 @@ sntp@2.x.x:
|
|||
dependencies:
|
||||
hoek "4.x.x"
|
||||
|
||||
sort-keys@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128"
|
||||
dependencies:
|
||||
is-plain-obj "^1.0.0"
|
||||
|
||||
source-map-resolve@^0.3.0:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.3.1.tgz#610f6122a445b8dd51535a2a71b783dfc1248761"
|
||||
|
@ -6720,6 +7033,10 @@ stream-to-observable@0.2.0:
|
|||
dependencies:
|
||||
any-observable "^0.2.0"
|
||||
|
||||
strict-uri-encode@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
|
||||
|
||||
string-length@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
||||
|
@ -7051,6 +7368,10 @@ time-stamp@^1.0.0:
|
|||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"
|
||||
|
||||
timed-out@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
|
||||
|
||||
tiny-emitter@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"
|
||||
|
@ -7302,6 +7623,20 @@ urix@^0.1.0, urix@~0.1.0:
|
|||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
|
||||
|
||||
url-join@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a"
|
||||
|
||||
url-parse-lax@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
|
||||
dependencies:
|
||||
prepend-http "^2.0.0"
|
||||
|
||||
url-to-options@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
|
||||
|
||||
use@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8"
|
||||
|
@ -7598,6 +7933,13 @@ ws@2.0.x:
|
|||
dependencies:
|
||||
ultron "~1.1.0"
|
||||
|
||||
ws@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-4.1.0.tgz#a979b5d7d4da68bf54efe0408967c324869a7289"
|
||||
dependencies:
|
||||
async-limiter "~1.0.0"
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
xml-crypto@^0.10.1:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/xml-crypto/-/xml-crypto-0.10.1.tgz#f832f74ccf56f24afcae1163a1fcab44d96774a8"
|
||||
|
|
290
yarn.lock
290
yarn.lock
|
@ -178,12 +178,53 @@
|
|||
dependencies:
|
||||
any-observable "^0.3.0"
|
||||
|
||||
"@sindresorhus/is@^0.7.0":
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
|
||||
|
||||
"@sinonjs/formatio@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-2.0.0.tgz#84db7e9eb5531df18a8c5e0bfb6e449e55e654b2"
|
||||
dependencies:
|
||||
samsam "1.3.0"
|
||||
|
||||
"@slack/client@^4.2.2":
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@slack/client/-/client-4.2.2.tgz#f997f39780bbff9c2128816e8377230a5f6bd0d5"
|
||||
dependencies:
|
||||
"@types/delay" "^2.0.1"
|
||||
"@types/form-data" "^2.2.1"
|
||||
"@types/got" "^7.1.7"
|
||||
"@types/is-stream" "^1.1.0"
|
||||
"@types/loglevel" "^1.5.3"
|
||||
"@types/node" "^9.4.7"
|
||||
"@types/p-cancelable" "^0.3.0"
|
||||
"@types/p-queue" "^2.3.1"
|
||||
"@types/p-retry" "^1.0.1"
|
||||
"@types/retry" "^0.10.2"
|
||||
"@types/url-join" "^0.8.2"
|
||||
"@types/ws" "^4.0.1"
|
||||
delay "^2.0.0"
|
||||
eventemitter3 "^3.0.0"
|
||||
finity "^0.5.4"
|
||||
form-data "^2.3.1"
|
||||
got "^8.0.3"
|
||||
is-stream "^1.1.0"
|
||||
loglevel "^1.6.1"
|
||||
object.entries "^1.0.4"
|
||||
object.getownpropertydescriptors "^2.0.3"
|
||||
object.values "^1.0.4"
|
||||
p-cancelable "^0.3.0"
|
||||
p-queue "^2.3.0"
|
||||
p-retry "^1.0.0"
|
||||
retry "^0.10.1"
|
||||
url-join "^4.0.0"
|
||||
ws "^4.1.0"
|
||||
|
||||
"@types/delay@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901"
|
||||
|
||||
"@types/eslint@^4.16.2":
|
||||
version "4.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-4.16.2.tgz#30f4f026019eb78a6ef12f276b75cd16ea2afb27"
|
||||
|
@ -205,6 +246,12 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/form-data@^2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/getopts@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/getopts/-/getopts-2.0.0.tgz#8a603370cb367d3192bd8012ad39ab2320b5b476"
|
||||
|
@ -217,6 +264,18 @@
|
|||
"@types/minimatch" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/got@^7.1.7":
|
||||
version "7.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/got/-/got-7.1.8.tgz#c5f421b25770689bf8948b1241f710d71a00d7dd"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/is-stream@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/json-schema@*":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-6.0.1.tgz#a761975746f1c1b2579c62e3a4b5e88f986f7e2e"
|
||||
|
@ -231,6 +290,10 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/loglevel@^1.5.3":
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/loglevel/-/loglevel-1.5.3.tgz#adfce55383edc5998a2170ad581b3e23d6adb5b8"
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
|
@ -243,6 +306,39 @@
|
|||
version "9.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.7.tgz#57d81cd98719df2c9de118f2d5f3b1120dcd7275"
|
||||
|
||||
"@types/node@^9.4.7":
|
||||
version "9.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.18.tgz#092e13ef64c47e986802c9c45a61c1454813b31d"
|
||||
|
||||
"@types/p-cancelable@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/p-cancelable/-/p-cancelable-0.3.0.tgz#3e4fcc54a3dfd81d0f5b93546bb68d0df50553bb"
|
||||
|
||||
"@types/p-queue@^2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/p-queue/-/p-queue-2.3.1.tgz#2fb251e46e884e31c4bd1bf58f0e188972353ff4"
|
||||
|
||||
"@types/p-retry@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/p-retry/-/p-retry-1.0.1.tgz#2302bc3da425014208c8a9b68293d37325124785"
|
||||
dependencies:
|
||||
"@types/retry" "*"
|
||||
|
||||
"@types/retry@*", "@types/retry@^0.10.2":
|
||||
version "0.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.10.2.tgz#bd1740c4ad51966609b058803ee6874577848b37"
|
||||
|
||||
"@types/url-join@^0.8.2":
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/url-join/-/url-join-0.8.2.tgz#1181ecbe1d97b7034e0ea1e35e62e86cc26b422d"
|
||||
|
||||
"@types/ws@^4.0.1":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-4.0.2.tgz#b29037627dd7ba31ec49a4f1584840422efb856f"
|
||||
dependencies:
|
||||
"@types/events" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
JSONStream@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.1.1.tgz#c98bfd88c8f1e1e8694e53c5baa6c8691553e59a"
|
||||
|
@ -2075,6 +2171,18 @@ cache-loader@1.0.3:
|
|||
loader-utils "^1.1.0"
|
||||
mkdirp "^0.5.1"
|
||||
|
||||
cacheable-request@^2.1.1:
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d"
|
||||
dependencies:
|
||||
clone-response "1.0.2"
|
||||
get-stream "3.0.0"
|
||||
http-cache-semantics "3.8.1"
|
||||
keyv "3.0.0"
|
||||
lowercase-keys "1.0.0"
|
||||
normalize-url "2.0.1"
|
||||
responselike "1.0.2"
|
||||
|
||||
call-me-maybe@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
|
||||
|
@ -2474,6 +2582,12 @@ clone-buffer@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
|
||||
|
||||
clone-response@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
|
||||
dependencies:
|
||||
mimic-response "^1.0.0"
|
||||
|
||||
clone-stats@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
|
||||
|
@ -3406,6 +3520,12 @@ decode-uri-component@^0.2.0:
|
|||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||
|
||||
decompress-response@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
|
||||
dependencies:
|
||||
mimic-response "^1.0.0"
|
||||
|
||||
decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1"
|
||||
|
@ -3535,6 +3655,12 @@ del@^3.0.0:
|
|||
pify "^3.0.0"
|
||||
rimraf "^2.2.8"
|
||||
|
||||
delay@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delay/-/delay-2.0.0.tgz#9112eadc03e4ec7e00297337896f273bbd91fae5"
|
||||
dependencies:
|
||||
p-defer "^1.0.0"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
|
@ -4417,6 +4543,10 @@ eventemitter3@1.x.x:
|
|||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
|
||||
|
||||
eventemitter3@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
|
||||
|
||||
events@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
|
||||
|
@ -4850,6 +4980,10 @@ findup-sync@~0.3.0:
|
|||
dependencies:
|
||||
glob "~5.0.0"
|
||||
|
||||
finity@^0.5.4:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/finity/-/finity-0.5.4.tgz#f2a8a9198e8286467328ec32c8bfcc19a2229c11"
|
||||
|
||||
flat-cache@^1.2.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481"
|
||||
|
@ -4958,6 +5092,13 @@ fragment-cache@^0.2.1:
|
|||
dependencies:
|
||||
map-cache "^0.2.2"
|
||||
|
||||
from2@^2.1.1:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
|
||||
dependencies:
|
||||
inherits "^2.0.1"
|
||||
readable-stream "^2.0.0"
|
||||
|
||||
from@^0.1.3, from@~0:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
|
||||
|
@ -5106,6 +5247,10 @@ get-stdin@^5.0.1:
|
|||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398"
|
||||
|
||||
get-stream@3.0.0, get-stream@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
|
||||
|
||||
get-stream@^2.2.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de"
|
||||
|
@ -5113,10 +5258,6 @@ get-stream@^2.2.0:
|
|||
object-assign "^4.0.1"
|
||||
pinkie-promise "^2.0.0"
|
||||
|
||||
get-stream@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
|
||||
|
||||
get-value@^2.0.3, get-value@^2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
|
||||
|
@ -5416,6 +5557,28 @@ got@^6.3.0, got@^6.7.1:
|
|||
unzip-response "^2.0.1"
|
||||
url-parse-lax "^1.0.0"
|
||||
|
||||
got@^8.0.3:
|
||||
version "8.3.1"
|
||||
resolved "https://registry.yarnpkg.com/got/-/got-8.3.1.tgz#093324403d4d955f5a16a7a8d39955d055ae10ed"
|
||||
dependencies:
|
||||
"@sindresorhus/is" "^0.7.0"
|
||||
cacheable-request "^2.1.1"
|
||||
decompress-response "^3.3.0"
|
||||
duplexer3 "^0.1.4"
|
||||
get-stream "^3.0.0"
|
||||
into-stream "^3.1.0"
|
||||
is-retry-allowed "^1.1.0"
|
||||
isurl "^1.0.0-alpha5"
|
||||
lowercase-keys "^1.0.0"
|
||||
mimic-response "^1.0.0"
|
||||
p-cancelable "^0.4.0"
|
||||
p-timeout "^2.0.1"
|
||||
pify "^3.0.0"
|
||||
safe-buffer "^5.1.1"
|
||||
timed-out "^4.0.1"
|
||||
url-parse-lax "^3.0.0"
|
||||
url-to-options "^1.0.1"
|
||||
|
||||
graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6:
|
||||
version "4.1.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
|
||||
|
@ -5886,6 +6049,10 @@ htmlparser2@^3.9.1:
|
|||
inherits "^2.0.1"
|
||||
readable-stream "^2.0.2"
|
||||
|
||||
http-cache-semantics@3.8.1:
|
||||
version "3.8.1"
|
||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
|
||||
|
||||
http-errors@1.6.2, http-errors@~1.6.2:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
|
||||
|
@ -6179,6 +6346,13 @@ interpret@^1.0.0:
|
|||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
|
||||
|
||||
into-stream@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6"
|
||||
dependencies:
|
||||
from2 "^2.1.1"
|
||||
p-is-promise "^1.1.0"
|
||||
|
||||
invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
|
@ -6541,7 +6715,7 @@ is-resolvable@^1.0.0:
|
|||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
|
||||
|
||||
is-retry-allowed@^1.0.0:
|
||||
is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
|
||||
|
||||
|
@ -7267,6 +7441,10 @@ jsesc@~0.5.0:
|
|||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
|
||||
|
||||
json-buffer@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
|
||||
|
||||
json-loader@^0.5.4:
|
||||
version "0.5.7"
|
||||
resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
|
||||
|
@ -7472,6 +7650,12 @@ keymirror@^0.1.1:
|
|||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/keymirror/-/keymirror-0.1.1.tgz#918889ea13f8d0a42e7c557250eee713adc95c35"
|
||||
|
||||
keyv@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373"
|
||||
dependencies:
|
||||
json-buffer "3.0.0"
|
||||
|
||||
kilt@2.x.x:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/kilt/-/kilt-2.0.2.tgz#04d7183c298a1232efddf7ddca5959a8f6301e20"
|
||||
|
@ -8094,6 +8278,10 @@ log4js@^0.6.31:
|
|||
readable-stream "~1.0.2"
|
||||
semver "~4.3.3"
|
||||
|
||||
loglevel@^1.6.1:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa"
|
||||
|
||||
lolex@^2.2.0, lolex@^2.3.2:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.6.0.tgz#cf9166f3c9dece3cdeb5d6b01fce50f14a1203e3"
|
||||
|
@ -8115,7 +8303,7 @@ loud-rejection@^1.0.0:
|
|||
currently-unhandled "^0.4.1"
|
||||
signal-exit "^3.0.0"
|
||||
|
||||
lowercase-keys@^1.0.0:
|
||||
lowercase-keys@1.0.0, lowercase-keys@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
|
||||
|
||||
|
@ -8383,6 +8571,10 @@ mimic-fn@^1.0.0:
|
|||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
|
||||
|
||||
mimic-response@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e"
|
||||
|
||||
mimos@3.x.x:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/mimos/-/mimos-3.0.3.tgz#b9109072ad378c2b72f6a0101c43ddfb2b36641f"
|
||||
|
@ -8727,6 +8919,10 @@ node-pre-gyp@^0.6.39:
|
|||
tar "^2.2.1"
|
||||
tar-pack "^3.4.0"
|
||||
|
||||
nodemailer@^4.6.4:
|
||||
version "4.6.4"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.6.4.tgz#f0d72d0c6a6ec5f4369fa8f4bf5127a31baa2014"
|
||||
|
||||
nomnom@~1.6.2:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.6.2.tgz#84a66a260174408fc5b77a18f888eccc44fb6971"
|
||||
|
@ -8778,6 +8974,14 @@ normalize-range@^0.1.2:
|
|||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
|
||||
|
||||
normalize-url@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6"
|
||||
dependencies:
|
||||
prepend-http "^2.0.0"
|
||||
query-string "^5.0.1"
|
||||
sort-keys "^2.0.0"
|
||||
|
||||
normalize-url@^1.4.0:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
|
||||
|
@ -9045,10 +9249,26 @@ osenv@^0.1.0, osenv@^0.1.4:
|
|||
os-homedir "^1.0.0"
|
||||
os-tmpdir "^1.0.0"
|
||||
|
||||
p-cancelable@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
|
||||
|
||||
p-cancelable@^0.4.0:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0"
|
||||
|
||||
p-defer@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
|
||||
|
||||
p-finally@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
|
||||
|
||||
p-is-promise@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
|
||||
|
||||
p-limit@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
|
||||
|
@ -9065,6 +9285,22 @@ p-map@^1.1.1:
|
|||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
|
||||
|
||||
p-queue@^2.3.0:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-2.4.2.tgz#03609826682b743be9a22dba25051bd46724fc34"
|
||||
|
||||
p-retry@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-1.0.0.tgz#3927332a4b7d70269b535515117fc547da1a6968"
|
||||
dependencies:
|
||||
retry "^0.10.0"
|
||||
|
||||
p-timeout@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038"
|
||||
dependencies:
|
||||
p-finally "^1.0.0"
|
||||
|
||||
p-try@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
|
||||
|
@ -9734,6 +9970,10 @@ prepend-http@^1.0.0, prepend-http@^1.0.1:
|
|||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||
|
||||
prepend-http@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
|
||||
preserve@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
|
||||
|
@ -9944,6 +10184,14 @@ query-string@^4.1.0:
|
|||
object-assign "^4.1.0"
|
||||
strict-uri-encode "^1.0.0"
|
||||
|
||||
query-string@^5.0.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
|
||||
dependencies:
|
||||
decode-uri-component "^0.2.0"
|
||||
object-assign "^4.1.0"
|
||||
strict-uri-encode "^1.0.0"
|
||||
|
||||
querystring-browser@1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/querystring-browser/-/querystring-browser-1.0.4.tgz#f2e35881840a819bc7b1bf597faf0979e6622dc6"
|
||||
|
@ -10834,6 +11082,12 @@ resolve@~0.3.1:
|
|||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-0.3.1.tgz#34c63447c664c70598d1c9b126fc43b2a24310a4"
|
||||
|
||||
responselike@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
|
||||
dependencies:
|
||||
lowercase-keys "^1.0.0"
|
||||
|
||||
restore-cursor@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
|
||||
|
@ -10858,6 +11112,10 @@ ret@~0.1.10:
|
|||
version "0.1.15"
|
||||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||
|
||||
retry@^0.10.0, retry@^0.10.1:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
|
||||
|
||||
right-align@^0.1.1:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
|
||||
|
@ -11301,6 +11559,12 @@ sort-keys@^1.0.0:
|
|||
dependencies:
|
||||
is-plain-obj "^1.0.0"
|
||||
|
||||
sort-keys@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128"
|
||||
dependencies:
|
||||
is-plain-obj "^1.0.0"
|
||||
|
||||
source-list-map@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
|
||||
|
@ -12025,7 +12289,7 @@ timed-out@^2.0.0:
|
|||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a"
|
||||
|
||||
timed-out@^4.0.0:
|
||||
timed-out@^4.0.0, timed-out@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
|
||||
|
||||
|
@ -12623,6 +12887,10 @@ urix@^0.1.0, urix@~0.1.0:
|
|||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
|
||||
|
||||
url-join@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a"
|
||||
|
||||
url-loader@0.5.9:
|
||||
version "0.5.9"
|
||||
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.5.9.tgz#cc8fea82c7b906e7777019250869e569e995c295"
|
||||
|
@ -12636,6 +12904,12 @@ url-parse-lax@^1.0.0:
|
|||
dependencies:
|
||||
prepend-http "^1.0.1"
|
||||
|
||||
url-parse-lax@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
|
||||
dependencies:
|
||||
prepend-http "^2.0.0"
|
||||
|
||||
url-regex@^3.0.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/url-regex/-/url-regex-3.2.0.tgz#dbad1e0c9e29e105dd0b1f09f6862f7fdb482724"
|
||||
|
@ -13383,7 +13657,7 @@ ws@2.0.x:
|
|||
dependencies:
|
||||
ultron "~1.1.0"
|
||||
|
||||
ws@^4.0.0:
|
||||
ws@^4.0.0, ws@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-4.1.0.tgz#a979b5d7d4da68bf54efe0408967c324869a7289"
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in a new issue