Merge pull request #3558 from serverless/addLogin

Add login command
This commit is contained in:
Eslam λ Hefnawy 2017-05-24 20:02:49 +07:00 committed by GitHub
commit bb7700dd0d
38 changed files with 1475 additions and 461 deletions

View File

@ -62,6 +62,7 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se
<li><a href="./cli-reference/invoke.md">Invoke</a></li>
<li><a href="./cli-reference/invoke-local.md">Invoke Local</a></li>
<li><a href="./cli-reference/logs.md">Logs</a></li>
<li><a href="./cli-reference/login.md">Login</a></li>
<li><a href="./cli-reference/metrics.md">Metrics</a></li>
<li><a href="./cli-reference/info.md">Info</a></li>
<li><a href="./cli-reference/rollback.md">Rollback</a></li>

View File

@ -0,0 +1,24 @@
<!--
title: Serverless Framework Commands - Login
menuText: login
menuOrder: 11
description: Login to the serverless platform
layout: Doc
-->
<!-- DOCS-SITE-LINK:START automatically generated -->
### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/aws/cli-reference/login)
<!-- DOCS-SITE-LINK:END -->
# Login
The `login` command logs users into the serverless platform.
It will create a new serverless platform account if one doesn't already exist.
```bash
serverless login
# Shorthand
sls login
```

View File

@ -54,6 +54,7 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se
<li><a href="./cli-reference/deploy.md">Deploy</a></li>
<li><a href="./cli-reference/deploy-function.md">Deploy Function</a></li>
<li><a href="./cli-reference/invoke.md">Invoke</a></li>
<li><a href="./cli-reference/login.md">Login</a></li>
<li><a href="./cli-reference/logs.md">Logs</a></li>
<li><a href="./cli-reference/remove.md">Remove</a></li>
</ul>

View File

@ -0,0 +1,24 @@
<!--
title: Serverless Framework Commands - Login
menuText: login
menuOrder: 11
description: Login to the serverless platform
layout: Doc
-->
<!-- DOCS-SITE-LINK:START automatically generated -->
### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/azure/cli-reference/login)
<!-- DOCS-SITE-LINK:END -->
# Login
The `login` command logs users into the serverless platform.
It will create a new serverless platform account if one doesn't already exist.
```bash
serverless login
# Shorthand
sls login
```

View File

@ -54,6 +54,7 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se
<li><a href="./cli-reference/deploy.md">Deploy</a></li>
<li><a href="./cli-reference/info.md">Info</a></li>
<li><a href="./cli-reference/invoke.md">Invoke</a></li>
<li><a href="./cli-reference/login.md">Login</a></li>
<li><a href="./cli-reference/logs.md">Logs</a></li>
<li><a href="./cli-reference/remove.md">Remove</a></li>
</ul>

View File

@ -0,0 +1,24 @@
<!--
title: Serverless Framework Commands - Login
menuText: login
menuOrder: 11
description: Login to the serverless platform
layout: Doc
-->
<!-- DOCS-SITE-LINK:START automatically generated -->
### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/google/cli-reference/login)
<!-- DOCS-SITE-LINK:END -->
# Login
The `login` command logs users into the serverless platform.
It will create a new serverless platform account if one doesn't already exist.
```bash
serverless login
# Shorthand
sls login
```

View File

@ -59,6 +59,7 @@ If you have questions, join the [chat in gitter](https://gitter.im/serverless/se
<li><a href="./cli-reference/deploy-function.md">Deploy Function</a></li>
<li><a href="./cli-reference/invoke.md">Invoke</a></li>
<li><a href="./cli-reference/invoke-local.md">Invoke Local</a></li>
<li><a href="./cli-reference/login.md">Login</a></li>
<li><a href="./cli-reference/logs.md">Logs</a></li>
<li><a href="./cli-reference/info.md">Info</a></li>
<li><a href="./cli-reference/remove.md">Remove</a></li>

View File

@ -0,0 +1,24 @@
<!--
title: Serverless Framework Commands - Login
menuText: login
menuOrder: 11
description: Login to the serverless platform
layout: Doc
-->
<!-- DOCS-SITE-LINK:START automatically generated -->
### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/providers/openwhisk/cli-reference/login)
<!-- DOCS-SITE-LINK:END -->
# Login
The `login` command logs users into the serverless platform.
It will create a new serverless platform account if one doesn't already exist.
```bash
serverless login
# Shorthand
sls login
```

View File

@ -2,15 +2,19 @@
const fs = require('fs');
const path = require('path');
const YAML = require('js-yaml');
const ci = require('ci-info');
const BbPromise = require('bluebird');
const fse = BbPromise.promisifyAll(require('fs-extra'));
const _ = require('lodash');
const fetch = require('node-fetch');
const uuid = require('uuid');
const os = require('os');
const fileExistsSync = require('../utils/fs/fileExistsSync');
const writeFileSync = require('../utils/fs/writeFileSync');
const readFileSync = require('../utils/fs/readFileSync');
const isDockerContainer = require('../utils/isDockerContainer');
const version = require('../../package.json').version;
const segment = require('../utils/segment');
const configUtils = require('../utils/config');
class Utils {
constructor(serverless) {
@ -31,35 +35,15 @@ class Utils {
}
fileExistsSync(filePath) {
try {
const stats = fse.lstatSync(filePath);
return stats.isFile();
} catch (e) {
return false;
}
return fileExistsSync(filePath);
}
writeFileDir(filePath) {
return fse.mkdirsSync(path.dirname(filePath));
}
writeFileSync(filePath, conts) {
let contents = conts || '';
fse.mkdirsSync(path.dirname(filePath));
if (filePath.indexOf('.json') !== -1 && typeof contents !== 'string') {
contents = JSON.stringify(contents, null, 2);
}
const yamlFileExists = (filePath.indexOf('.yaml') !== -1);
const ymlFileExists = (filePath.indexOf('.yml') !== -1);
if ((yamlFileExists || ymlFileExists) && typeof contents !== 'string') {
contents = YAML.dump(contents);
}
return fse.writeFileSync(filePath, contents);
writeFileSync(filePath, contents) {
return writeFileSync(filePath, contents);
}
writeFile(filePath, contents) {
@ -88,21 +72,7 @@ class Utils {
}
readFileSync(filePath) {
let contents;
// Read file
contents = fse.readFileSync(filePath);
// Auto-parse JSON
if (filePath.endsWith('.json')) {
contents = JSON.parse(contents);
} else if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) {
contents = YAML.load(contents.toString(), { filename: filePath });
} else {
contents = contents.toString().trim();
}
return contents;
return readFileSync(filePath);
}
readFile(filePath) {
@ -164,23 +134,15 @@ class Utils {
// the context in which serverless was executed (e.g. "install", "usage", "uninstall", ...)
context = context || 'usage'; //eslint-disable-line
const log = (data) => {
const writeKey = 'XXXX'; // TODO: Replace me before release
const auth = `${writeKey}:`;
// Service values
const service = serverless.service;
const resources = service.resources;
const provider = service.provider;
const functions = service.functions;
return fetch('https://api.segment.io/v1/track', {
headers: {
Authorization: `Basic ${new Buffer(auth).toString('base64')}`,
'content-type': 'application/json',
},
method: 'POST',
timeout: '1000',
body: JSON.stringify(data),
})
.then((response) => response.json())
.then(() => BbPromise.resolve())
.catch(() => BbPromise.resolve());
};
// CLI inputs
const options = serverless.processedInput.options;
const commands = serverless.processedInput.commands;
return new BbPromise((resolve) => {
const serverlessDirPath = path.join(os.homedir(), '.serverless');
@ -190,7 +152,7 @@ class Utils {
if (this.fileExistsSync(statsDisabledFilePath)) {
return resolve();
}
// Todo update with new user id
let userId = uuid.v1();
if (!this.fileExistsSync(statsEnabledFilePath)) {
@ -200,7 +162,6 @@ class Utils {
}
// filter out the whitelisted options
const options = serverless.processedInput.options;
const whitelistedOptionKeys = ['help', 'disable', 'enable'];
const optionKeys = Object.keys(options);
@ -214,11 +175,11 @@ class Utils {
});
// function related information retrieval
const numberOfFunctions = _.size(serverless.service.functions);
const numberOfFunctions = _.size(functions);
const memorySizeAndTimeoutPerFunction = [];
if (numberOfFunctions) {
_.forEach(serverless.service.functions, (func) => {
_.forEach(functions, (func) => {
const memorySize = Number(func.memorySize)
|| Number(this.serverless.service.provider.memorySize)
|| 1024;
@ -239,7 +200,7 @@ class Utils {
const numberOfEventsPerType = [];
const eventNamesPerFunction = [];
if (numberOfFunctions) {
_.forEach(serverless.service.functions, (func) => {
_.forEach(functions, (func) => {
if (func.events) {
const funcEventsArray = [];
@ -264,18 +225,12 @@ class Utils {
}
let hasCustomResourcesDefined = false;
// check if configuration in resources.Resources is defined
if ((serverless.service.resources &&
serverless.service.resources.Resources &&
Object.keys(serverless.service.resources.Resources).length)) {
if ((resources && resources.Resources && Object.keys(resources.Resources).length)) {
hasCustomResourcesDefined = true;
}
// check if configuration in resources.Outputs is defined
if ((serverless.service.resources &&
serverless.service.resources.Outputs &&
Object.keys(serverless.service.resources.Outputs).length)) {
if ((resources && resources.Outputs && Object.keys(resources.Outputs).length)) {
hasCustomResourcesDefined = true;
}
@ -283,43 +238,32 @@ class Utils {
const defaultVariableSyntax = '\\${([ :a-zA-Z0-9._,\\-\\/\\(\\)]+?)}';
// check if the variableSyntax in the provider section is defined
if (serverless.service.provider &&
serverless.service.provider.variableSyntax &&
serverless.service.provider.variableSyntax !== defaultVariableSyntax) {
if (provider && provider.variableSyntax
&& provider.variableSyntax !== defaultVariableSyntax) {
hasCustomVariableSyntaxDefined = true;
}
// wrap in try catch to make sure that missing permissions won't break anything
let isDockerContainer = false;
try {
const cgroupFilePath = path.join('/', 'proc', '1', 'cgroup');
const cgroupFileContent = fs.readFileSync(cgroupFilePath).toString();
isDockerContainer = !!cgroupFileContent.match(/docker/);
} catch (exception) {
// do nothing
}
const data = {
userId,
event: 'framework_stat',
properties: {
version: 2,
command: {
name: serverless.processedInput.commands.join(' '),
name: commands.join(' '),
filteredOptions,
isRunInService: (!!serverless.config.servicePath),
},
service: {
numberOfCustomPlugins: _.size(serverless.service.plugins),
numberOfCustomPlugins: _.size(service.plugins),
hasCustomResourcesDefined,
hasVariablesInCustomSectionDefined: (!!serverless.service.custom),
hasVariablesInCustomSectionDefined: (!!service.custom),
hasCustomVariableSyntaxDefined,
},
provider: {
name: serverless.service.provider.name,
runtime: serverless.service.provider.runtime,
stage: serverless.service.provider.stage,
region: serverless.service.provider.region,
name: provider.name,
runtime: provider.runtime,
stage: provider.stage,
region: provider.region,
},
functions: {
numberOfFunctions,
@ -339,17 +283,24 @@ class Utils {
userAgent: (process.env.SERVERLESS_DASHBOARD) ? 'dashboard' : 'cli',
serverlessVersion: serverless.version,
nodeJsVersion: process.version,
isDockerContainer,
isDockerContainer: isDockerContainer(),
isCISystem: ci.isCI,
ciSystem: ci.name,
},
},
};
const config = configUtils.getConfig();
if (config.userId && data.properties && data.properties.general) {
data.properties.general.platformId = config.userId;
}
return resolve(data);
}).then((data) => {
// only log the data if it's there
if (data) log(data);
if (data) {
segment.track(data);
}
});
}
}

View File

@ -6,21 +6,22 @@ const expect = require('chai').expect;
const fse = require('fs-extra');
const fs = require('fs');
const sinon = require('sinon');
const proxyquire = require('proxyquire');
const Serverless = require('../../lib/Serverless');
const testUtils = require('../../tests/utils');
const serverlessVersion = require('../../package.json').version;
const segment = require('../utils/segment');
const proxyquire = require('proxyquire');
describe('Utils', () => {
let utils;
let serverless;
let fetchStub;
let isDockerContainerStub;
let Utils;
beforeEach(() => {
fetchStub = sinon.stub().resolves();
isDockerContainerStub = sinon.stub().returns(true);
Utils = proxyquire('../../lib/classes/Utils.js', {
'node-fetch': fetchStub,
'../utils/isDockerContainer': isDockerContainerStub,
});
serverless = new Serverless();
utils = new Utils(serverless);
@ -294,6 +295,7 @@ describe('Utils', () => {
describe('#logStat()', () => {
let serverlessDirPath;
let homeDir;
let trackStub;
beforeEach(() => {
serverless.init();
@ -313,6 +315,12 @@ describe('Utils', () => {
// set the properties for the processed inputs
serverless.processedInput.commands = [];
serverless.processedInput.options = {};
trackStub = sinon.stub(segment, 'track');
});
afterEach(() => {
segment.track.restore();
});
it('should resolve if a file called stats-disabled is present', () => {
@ -323,7 +331,7 @@ describe('Utils', () => {
);
return utils.logStat(serverless).then(() => {
expect(fetchStub.calledOnce).to.equal(false);
expect(trackStub.calledOnce).to.equal(false);
});
});
@ -358,36 +366,23 @@ describe('Utils', () => {
serverless.processedInput.options = options;
return utils.logStat(serverless).then(() => {
expect(fetchStub.calledOnce).to.equal(true);
expect(fetchStub.args[0][0]).to.equal('https://api.segment.io/v1/track');
expect(fetchStub.args[0][1].method).to.equal('POST');
expect(fetchStub.args[0][1].timeout).to.equal('1000');
expect(trackStub.calledOnce).to.equal(true);
const parsedBody = JSON.parse(fetchStub.args[0][1].body);
const data = trackStub.args[0][0];
expect(parsedBody.properties.command.filteredOptions)
.to.deep.equal({ help: true });
expect(data.properties.command.filteredOptions).to.deep.equal({ help: true });
});
});
it('should be able to detect Docker containers', () => {
const cgroupFileContent = '6:devices:/docker/3601745b3bd54d9780436faa5f0e4f72';
const cgroupFilePath = path.join('/', 'proc', '1', 'cgroup');
const cgroupFileContentStub = sinon.stub(fs, 'readFileSync')
.withArgs(cgroupFilePath)
.returns(cgroupFileContent);
it('should be able to detect Docker containers', () => utils
.logStat(serverless).then(() => {
expect(isDockerContainerStub.calledOnce).to.equal(true);
expect(trackStub.calledOnce).to.equal(true);
utils.logStat(serverless).then(() => {
expect(cgroupFileContentStub.calledOnce).to.equal(true);
const parsedBody = JSON.parse(fetchStub.args[0][1].body);
expect(parsedBody.properties.general.isDockerContainer)
.to.equal(true);
fs.readFileSync.restore();
});
});
const data = trackStub.args[0][0];
expect(data.properties.general.isDockerContainer).to.equal(true);
})
);
it('should send the gathered information', () => {
serverless.service = {
@ -436,55 +431,52 @@ describe('Utils', () => {
};
return utils.logStat(serverless).then(() => {
expect(fetchStub.calledOnce).to.equal(true);
expect(fetchStub.args[0][0]).to.equal('https://api.segment.io/v1/track');
expect(fetchStub.args[0][1].method).to.equal('POST');
expect(fetchStub.args[0][1].timeout).to.equal('1000');
expect(trackStub.calledOnce).to.equal(true);
const parsedBody = JSON.parse(fetchStub.args[0][1].body);
const data = trackStub.args[0][0];
expect(parsedBody.userId.length).to.be.at.least(1);
expect(data.userId.length).to.be.at.least(1);
// command property
expect(parsedBody.properties.command.name)
expect(data.properties.command.name)
.to.equal('');
expect(parsedBody.properties.command
expect(data.properties.command
.isRunInService).to.equal(false); // false because CWD is not a service
expect(parsedBody.properties.command.filteredOptions)
expect(data.properties.command.filteredOptions)
.to.deep.equal({});
// service property
expect(parsedBody.properties.service.numberOfCustomPlugins).to.equal(0);
expect(parsedBody.properties.service.hasCustomResourcesDefined).to.equal(true);
expect(parsedBody.properties.service.hasVariablesInCustomSectionDefined).to.equal(false);
expect(parsedBody.properties.service.hasCustomVariableSyntaxDefined).to.equal(true);
expect(data.properties.service.numberOfCustomPlugins).to.equal(0);
expect(data.properties.service.hasCustomResourcesDefined).to.equal(true);
expect(data.properties.service.hasVariablesInCustomSectionDefined).to.equal(false);
expect(data.properties.service.hasCustomVariableSyntaxDefined).to.equal(true);
// functions property
expect(parsedBody.properties.functions.numberOfFunctions).to.equal(2);
expect(parsedBody.properties.functions.memorySizeAndTimeoutPerFunction[0]
expect(data.properties.functions.numberOfFunctions).to.equal(2);
expect(data.properties.functions.memorySizeAndTimeoutPerFunction[0]
.memorySize).to.equal(1024);
expect(parsedBody.properties.functions.memorySizeAndTimeoutPerFunction[0]
expect(data.properties.functions.memorySizeAndTimeoutPerFunction[0]
.timeout).to.equal(6);
expect(parsedBody.properties.functions.memorySizeAndTimeoutPerFunction[1]
expect(data.properties.functions.memorySizeAndTimeoutPerFunction[1]
.memorySize).to.equal(16);
expect(parsedBody.properties.functions.memorySizeAndTimeoutPerFunction[1]
expect(data.properties.functions.memorySizeAndTimeoutPerFunction[1]
.timeout).to.equal(200);
// events property
expect(parsedBody.properties.events.numberOfEvents).to.equal(3);
expect(parsedBody.properties.events.numberOfEventsPerType[0].name).to.equal('http');
expect(parsedBody.properties.events.numberOfEventsPerType[0].count).to.equal(2);
expect(parsedBody.properties.events.numberOfEventsPerType[1].name).to.equal('s3');
expect(parsedBody.properties.events.numberOfEventsPerType[1].count).to.equal(1);
expect(parsedBody.properties.events.numberOfEventsPerType[2].name).to.equal('sns');
expect(parsedBody.properties.events.numberOfEventsPerType[2].count).to.equal(1);
expect(parsedBody.properties.events.eventNamesPerFunction[0][0]).to.equal('http');
expect(parsedBody.properties.events.eventNamesPerFunction[0][1]).to.equal('s3');
expect(parsedBody.properties.events.eventNamesPerFunction[1][0]).to.equal('http');
expect(parsedBody.properties.events.eventNamesPerFunction[1][1]).to.equal('sns');
expect(data.properties.events.numberOfEvents).to.equal(3);
expect(data.properties.events.numberOfEventsPerType[0].name).to.equal('http');
expect(data.properties.events.numberOfEventsPerType[0].count).to.equal(2);
expect(data.properties.events.numberOfEventsPerType[1].name).to.equal('s3');
expect(data.properties.events.numberOfEventsPerType[1].count).to.equal(1);
expect(data.properties.events.numberOfEventsPerType[2].name).to.equal('sns');
expect(data.properties.events.numberOfEventsPerType[2].count).to.equal(1);
expect(data.properties.events.eventNamesPerFunction[0][0]).to.equal('http');
expect(data.properties.events.eventNamesPerFunction[0][1]).to.equal('s3');
expect(data.properties.events.eventNamesPerFunction[1][0]).to.equal('http');
expect(data.properties.events.eventNamesPerFunction[1][1]).to.equal('sns');
// general property
expect(parsedBody.properties.general.userId.length).to.be.at.least(1);
expect(parsedBody.properties.general.timestamp).to.match(/[0-9]+/);
expect(parsedBody.properties.general.timezone.length).to.be.at.least(1);
expect(parsedBody.properties.general.operatingSystem.length).to.be.at.least(1);
expect(parsedBody.properties.general.serverlessVersion).to.equal(serverlessVersion);
expect(parsedBody.properties.general.nodeJsVersion.length).to.be.at.least(1);
expect(data.properties.general.userId.length).to.be.at.least(1);
expect(data.properties.general.timestamp).to.match(/[0-9]+/);
expect(data.properties.general.timezone.length).to.be.at.least(1);
expect(data.properties.general.operatingSystem.length).to.be.at.least(1);
expect(data.properties.general.serverlessVersion).to.equal(serverlessVersion);
expect(data.properties.general.nodeJsVersion.length).to.be.at.least(1);
});
});

View File

@ -8,6 +8,8 @@
"./invoke/invoke.js",
"./info/info.js",
"./logs/logs.js",
"./login/login.js",
"./logout/logout.js",
"./metrics/metrics.js",
"./remove/remove.js",
"./rollback/index.js",

View File

@ -4,6 +4,7 @@ const BbPromise = require('bluebird');
const path = require('path');
const fse = require('fs-extra');
const _ = require('lodash');
const userStats = require('../../utils/userStats');
// class wide constants
const validTemplates = [
@ -117,7 +118,9 @@ class Create {
});
}
if (notPlugin) this.serverless.config.update({ servicePath: process.cwd() });
if (notPlugin) {
this.serverless.config.update({ servicePath: process.cwd() });
}
// copy template files recursively to cwd
// while keeping template file tree
@ -152,6 +155,11 @@ class Create {
fse.writeFileSync(serverlessYmlFilePath, serverlessYmlFileContent);
}
userStats.track('service_created', {
template: this.options.template,
serviceName,
});
this.serverless.cli.asciiGreeting();
this.serverless.cli
.log(`Successfully generated boilerplate for template: "${this.options.template}"`);

View File

@ -1,6 +1,7 @@
'use strict';
const BbPromise = require('bluebird');
const userStats = require('../../utils/userStats');
class Deploy {
constructor(serverless, options) {
@ -88,8 +89,13 @@ class Deploy {
}
return BbPromise.resolve();
}),
'after:deploy:deploy': () => BbPromise.bind(this)
.then(this.track),
};
}
track() {
userStats.track('service_deployed');
}
}
module.exports = Deploy;

159
lib/plugins/login/login.js Normal file
View File

@ -0,0 +1,159 @@
'use strict';
const BbPromise = require('bluebird');
const crypto = require('crypto');
const readline = require('readline');
const fetch = require('node-fetch');
const jwtDecode = require('jwt-decode');
const chalk = require('chalk');
const openBrowser = require('../../utils/openBrowser');
const getFrameworkId = require('../../utils/getFrameworkId');
const clearConsole = require('../../utils/clearConsole');
const userStats = require('../../utils/userStats');
const setConfig = require('../../utils/config').set;
const config = {
AUTH0_CLIENT_ID: 'iiEYK0KB30gj94mjB8HP9lhhTgae0Rg3',
AUTH0_URL: 'https://serverlessinc.auth0.com',
AUTH0_CALLBACK_URL: 'https://serverless.com/auth',
};
function base64url(url) {
return url.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
class Login {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.commands = {
login: {
usage: 'Login or sign up for the Serverless Platform',
lifecycleEvents: [
'login',
],
},
};
this.hooks = {
'login:login': () => BbPromise.bind(this).then(this.login),
};
}
login() {
clearConsole();
this.serverless.cli.log('The Serverless login will open in your default browser...');
// Generate the verifier, and the corresponding challenge
const verifier = base64url(crypto.randomBytes(32));
const verifierChallenge = base64url(crypto.createHash('sha256').update(verifier).digest());
const frameworkId = getFrameworkId();
// eslint-disable-next-line prefer-template
const version = this.serverless.version;
const state = `id%3D${frameworkId}%26version%3D${version}%26platform%3D${process.platform}`;
// refresh token docs https://auth0.com/docs/tokens/preview/refresh-token#get-a-refresh-token
const authorizeUrl =
`${config.AUTH0_URL}/authorize?response_type=code&scope=openid%20profile%20offline_access` +
`&client_id=${config.AUTH0_CLIENT_ID}&redirect_uri=${config.AUTH0_CALLBACK_URL}` +
`&code_challenge=${verifierChallenge}&code_challenge_method=S256&state=${state}`;
setTimeout(() => {
this.serverless.cli.log('Opening browser...');
}, 300);
setTimeout(() => {
// pop open default browser
openBrowser(authorizeUrl);
// wait for token
const readlineInterface = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// o get an access token and a refresh token from that code you need to call the oauth/token endpoint. Here's more info - https://auth0.com/docs/protocols#3-getting-the-access-token
readlineInterface.question('Please enter the verification code here: ', (code) => {
const authorizationData = {
code,
code_verifier: verifier,
client_id: config.AUTH0_CLIENT_ID,
grant_type: 'authorization_code',
redirect_uri: config.AUTH0_CALLBACK_URL,
};
// verify login
fetch(`${config.AUTH0_URL}/oauth/token`, {
method: 'POST',
body: JSON.stringify(authorizationData),
headers: { 'content-type': 'application/json' },
})
.then((response) => response.json())
.then((platformResponse) => {
const decoded = jwtDecode(platformResponse.id_token);
this.serverless.cli.log('You are now logged in');
// because platform only support github
const id = decoded.original_user_id || decoded.sub;
/* For future use
segment.identify({
userId: id,
traits: {
email: profile.email,
},
}) */
const userConfig = {
userId: id,
frameworkId,
users: {},
};
// set user auth in global .serverlessrc file
userConfig.users[id] = {
userId: id,
name: decoded.name,
email: decoded.email,
auth: platformResponse,
};
// update .serverlessrc
setConfig(userConfig);
const userID = new Buffer(id).toString('base64');
const email = new Buffer(decoded.email).toString('base64');
const name = new Buffer(decoded.name).toString('base64');
const loginCount = decoded.login_count;
const createdAt = decoded.created_at;
const successUrl = `https://serverless.com/success?u=${userID}&e=${email}&n=${name}&c=${loginCount}&v=${version}&d=${createdAt}&id=${frameworkId}`; // eslint-disable-line
openBrowser(successUrl);
// identify user for better onboarding
userStats.identify({
id,
frameworkId,
email: decoded.email,
// unix timestamp
created_at: Math.round(+new Date(createdAt) / 1000),
force: true,
}).then(() => {
userStats.track('user_loggedIn', {
id,
email: decoded.email,
force: true,
}).then(() => {
// then exit process
process.exit(0);
});
});
})
.catch(() => {
this.serverless.cli.consoleLog(
chalk.red('Incorrect token value supplied. Please run "serverless login" again'));
process.exit(0);
});
});
}, 2000);
}
}
module.exports = Login;

View File

@ -0,0 +1,19 @@
'use strict';
const expect = require('chai').expect;
const Login = require('./login');
const Serverless = require('../../Serverless');
describe('Login', () => {
let login;
let serverless;
beforeEach(() => {
serverless = new Serverless();
login = new Login(serverless);
});
describe('#constructor()', () => {
it('should have commands', () => expect(login.commands).to.be.not.empty);
});
});

View File

@ -0,0 +1,53 @@
'use strict';
const userStats = require('../../utils/userStats');
const configUtils = require('../../utils/config');
class Login {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.commands = {
logout: {
usage: 'Logout from the Serverless Platform',
lifecycleEvents: ['logout'],
},
};
this.hooks = {
'logout:logout': this.logout.bind(this),
};
}
logout() {
const config = configUtils.getConfig();
const currentId = config.userId;
const globalConfig = configUtils.getGlobalConfig();
try {
// TODO Once we start using refresh tokens we also need to implement an API endpoint
// that invalidate a refresh token in Auth0 (using the Auth0 Management API).
if (globalConfig && globalConfig.users && globalConfig.users[currentId]) {
if (globalConfig.users[currentId].auth) {
// remove auth tokens from user
configUtils.set(`users.${currentId}.auth`, null);
// log stat
userStats.track('user_loggedOut').then(() => {
this.serverless.cli.consoleLog('Successfully logged out.');
process.exit(0);
});
} else {
this.serverless.cli.consoleLog('You are already logged out');
}
}
} catch (e) {
this.serverless.cli.consoleLog(
'Failed to logout. Please report bug in https://github.com/serverless/serverless/issues');
// Note no need to wait for any connections e.g. segment to close
process.exit(0);
}
}
}
module.exports = Login;

View File

@ -0,0 +1,19 @@
'use strict';
const expect = require('chai').expect;
const Logout = require('./logout');
const Serverless = require('../../Serverless');
describe('Logout', () => {
let logout;
let serverless;
beforeEach(() => {
serverless = new Serverless();
logout = new Logout(serverless);
});
describe('#constructor()', () => {
it('should have commands', () => expect(logout.commands).to.be.not.empty);
});
});

View File

@ -3,6 +3,8 @@
const path = require('path');
const fse = require('fs-extra');
const os = require('os');
const userStats = require('../../utils/userStats');
const setConfig = require('../../utils/config').set;
class SlStats {
constructor(serverless, options) {
@ -53,10 +55,20 @@ class SlStats {
const isStatsEnabled = this.options.enable && !this.options.disable;
const isStatsDisabled = this.options.disable && !this.options.enable;
if (isStatsEnabled) {
// stats file to be depricated in future release
this.createStatsFile(statsDisabledFilePath, statsEnabledFilePath);
// set new `trackingDisabled` key in .serverlessrc config
userStats.track('user_enabledTracking', { force: true }).then(() => {
setConfig('trackingDisabled', false);
});
this.serverless.cli.log('Stats successfully enabled');
} else if (isStatsDisabled) {
// stats file to be depricated in future release
this.createStatsFile(statsEnabledFilePath, statsDisabledFilePath);
// set new `trackingDisabled` key in .serverlessrc config
userStats.track('user_disabledTracking', { force: true }).then(() => {
setConfig('trackingDisabled', true);
});
this.serverless.cli.log('Stats successfully disabled');
}
} catch (error) {

View File

@ -0,0 +1,8 @@
'use strict';
/* Clear terminal output */
const clearConsole = function () {
process.stdout.write(process.platform !== 'win32' ? '\x1B[2J\x1B[3J\x1B[H' : '\x1Bc');
};
module.exports = clearConsole;

View File

@ -0,0 +1,54 @@
'use strict';
const expect = require('chai').expect;
const config = require('./index');
describe('Config', () => {
it('should have CONFIG_FILE_PATH', () => {
const configPath = config.CONFIG_FILE_PATH;
expect(configPath).to.exist; // eslint-disable-line
});
describe('When using config.getConfig', () => {
it('should have userId key', () => {
const conf = config.getConfig();
expect(conf).to.have.deep.property('userId');
});
it('should have frameworkId key', () => {
const conf = config.getConfig();
expect(conf).to.have.deep.property('frameworkId');
});
it('should have trackingDisabled key', () => {
const conf = config.getConfig();
expect(conf).to.have.deep.property('trackingDisabled');
});
});
describe('When using config.get', () => {
it('should have frameworkId', () => {
const frameworkId = config.get('frameworkId');
expect(frameworkId).to.exist; // eslint-disable-line
});
it('should have not have a value that doesnt exist', () => {
const doesntExist = config.get('frameworkIdzzzz');
expect(doesntExist).to.not.exist; // eslint-disable-line
});
});
describe('When using config.set', () => {
it('should add new properties with "set"', () => {
config.set('foo', true);
const foo = config.get('foo');
expect(foo).to.equal(true);
});
it('should delete properties with "delete"', () => {
// cleanup foo
config.delete('foo');
const zaz = config.get('foo');
expect(zaz).to.equal(undefined);
});
});
});

96
lib/utils/config/index.js Normal file
View File

@ -0,0 +1,96 @@
'use strict';
/* Config util */
const path = require('path');
const os = require('os');
const _ = require('lodash');
const writeFileAtomic = require('write-file-atomic');
const getFrameworkId = require('../getFrameworkId');
const fileExistsSync = require('../fs/fileExistsSync');
const readFileSync = require('../fs/readFileSync');
const isTrackingDisabled = require('../isTrackingDisabled');
const globalConfigPath = path.join(os.homedir(), '.serverlessrc');
function createConfig() {
// set default config options
const config = {
userId: null, // currentUserId
frameworkId: getFrameworkId(),
trackingDisabled: isTrackingDisabled(),
meta: {
created_at: Math.round(+new Date() / 1000),
updated_at: null,
},
};
writeFileAtomic.sync(globalConfigPath, JSON.stringify(config, null, 2));
return JSON.parse(readFileSync(globalConfigPath));
}
function getGlobalConfig() {
if (!fileExistsSync(globalConfigPath)) {
return createConfig();
}
const config = readFileSync(globalConfigPath);
// if global config empty add defaults
if (!config) {
return createConfig();
}
return JSON.parse(config);
}
// get .serverlessrc config + local config if exists
function getConfig() {
if (!fileExistsSync(globalConfigPath)) {
return createConfig();
}
return require('rc')('serverless'); // eslint-disable-line
}
// set global .serverlessrc config value.
function setConfigValue(key, value) {
let config = getGlobalConfig();
if (key && typeof key === 'string' && typeof value !== 'undefined') {
config = _.set(config, key, value);
} else if (_.isObject(key)) {
config = _.merge(config, key);
} else if (typeof value !== 'undefined') {
config = _.merge(config, value);
}
// update config meta
config.meta = config.meta || {};
config.meta.updated_at = Math.round(+new Date() / 1000);
// write file
writeFileAtomic.sync(globalConfigPath, JSON.stringify(config, null, 2));
return config;
}
function deleteConfigValue(key) {
let config = getGlobalConfig();
if (key && typeof key === 'string') {
config = _.omit(config, [key]);
} else if (key && _.isArray(key)) {
config = _.omit(config, key);
}
// write file
writeFileAtomic.sync(globalConfigPath, JSON.stringify(config, null, 2));
return config;
}
// set .serverlessrc config value
function getConfigValue(objectPath) {
const config = getConfig();
if (objectPath && typeof objectPath === 'string') {
return _.get(config, objectPath);
}
return config;
}
module.exports = {
set: setConfigValue,
get: getConfigValue,
delete: deleteConfigValue,
getConfig,
getGlobalConfig,
CONFIG_FILE_PATH: globalConfigPath,
};

View File

@ -0,0 +1,14 @@
'use strict';
const fse = require('./fse');
function fileExistsSync(filePath) {
try {
const stats = fse.lstatSync(filePath);
return stats.isFile();
} catch (e) {
return false;
}
}
module.exports = fileExistsSync;

View File

@ -0,0 +1,19 @@
'use strict';
const path = require('path');
const expect = require('chai').expect;
const fileExistsSync = require('./fileExistsSync');
describe('#fileExistsSync()', () => {
describe('When reading a file', () => {
it('should detect if a file exists', () => {
const file = fileExistsSync(__filename);
expect(file).to.equal(true);
});
it('should detect if a file doesn\'t exist', () => {
const noFile = fileExistsSync(path.join(__dirname, 'XYZ.json'));
expect(noFile).to.equal(false);
});
});
});

9
lib/utils/fs/fse.js Normal file
View File

@ -0,0 +1,9 @@
/**
* Promisified FSE
*/
'use strict';
const BbPromise = require('bluebird');
const fse = BbPromise.promisifyAll(require('fs-extra'));
module.exports = fse;

View File

@ -0,0 +1,24 @@
'use strict';
const YAML = require('js-yaml');
const fse = require('./fse');
function readFileSync(filePath) {
let contents;
// Read file
contents = fse.readFileSync(filePath);
// Auto-parse JSON
if (filePath.endsWith('.json')) {
contents = JSON.parse(contents);
} else if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) {
contents = YAML.load(contents.toString(), { filename: filePath });
} else {
contents = contents.toString().trim();
}
return contents;
}
module.exports = readFileSync;

View File

@ -0,0 +1,45 @@
'use strict';
const testUtils = require('../../../tests/utils');
const expect = require('chai').expect;
const writeFileSync = require('./writeFileSync');
const readFileSync = require('./readFileSync');
describe('#readFileSync()', () => {
it('should read a file synchronously', () => {
const tmpFilePath = testUtils.getTmpFilePath('anything.json');
writeFileSync(tmpFilePath, { foo: 'bar' });
const obj = readFileSync(tmpFilePath);
expect(obj.foo).to.equal('bar');
});
it('should read a filename extension .yml', () => {
const tmpFilePath = testUtils.getTmpFilePath('anything.yml');
writeFileSync(tmpFilePath, { foo: 'bar' });
const obj = readFileSync(tmpFilePath);
expect(obj.foo).to.equal('bar');
});
it('should read a filename extension .yaml', () => {
const tmpFilePath = testUtils.getTmpFilePath('anything.yaml');
writeFileSync(tmpFilePath, { foo: 'bar' });
const obj = readFileSync(tmpFilePath);
expect(obj.foo).to.equal('bar');
});
it('should throw YAMLException with filename if yml file is invalid format', () => {
const tmpFilePath = testUtils.getTmpFilePath('invalid.yml');
writeFileSync(tmpFilePath, ': a');
expect(() => {
readFileSync(tmpFilePath);
}).to.throw(new RegExp('YAMLException:.*invalid.yml'));
});
});

View File

@ -0,0 +1,26 @@
'use strict';
const path = require('path');
const YAML = require('js-yaml');
const fse = require('./fse');
function writeFileSync(filePath, conts) {
let contents = conts || '';
fse.mkdirsSync(path.dirname(filePath));
if (filePath.indexOf('.json') !== -1 && typeof contents !== 'string') {
contents = JSON.stringify(contents, null, 2);
}
const yamlFileExists = (filePath.indexOf('.yaml') !== -1);
const ymlFileExists = (filePath.indexOf('.yml') !== -1);
if ((yamlFileExists || ymlFileExists) && typeof contents !== 'string') {
contents = YAML.dump(contents);
}
return fse.writeFileSync(filePath, contents);
}
module.exports = writeFileSync;

View File

@ -0,0 +1,49 @@
'use strict';
const testUtils = require('../../../tests/utils');
const Serverless = require('../../../lib/Serverless');
const expect = require('chai').expect;
const writeFileSync = require('./writeFileSync');
const readFileSync = require('./readFileSync');
describe('#writeFileSync()', () => {
let serverless;
beforeEach(() => {
serverless = new Serverless();
serverless.init();
});
it('should write a .json file synchronously', () => {
const tmpFilePath = testUtils.getTmpFilePath('anything.json');
writeFileSync(tmpFilePath, { foo: 'bar' });
const obj = readFileSync(tmpFilePath);
expect(obj.foo).to.equal('bar');
});
it('should write a .yml file synchronously', () => {
const tmpFilePath = testUtils.getTmpFilePath('anything.yml');
writeFileSync(tmpFilePath, { foo: 'bar' });
return serverless.yamlParser.parse(tmpFilePath).then((obj) => {
expect(obj.foo).to.equal('bar');
});
});
it('should write a .yaml file synchronously', () => {
const tmpFilePath = testUtils.getTmpFilePath('anything.yaml');
writeFileSync(tmpFilePath, { foo: 'bar' });
return serverless.yamlParser.parse(tmpFilePath).then((obj) => {
expect(obj.foo).to.equal('bar');
});
});
it('should throw error if invalid path is provided', () => {
expect(() => { writeFileSync(null); }).to.throw(Error);
});
});

View File

@ -0,0 +1,29 @@
'use strict';
const path = require('path');
const getServerlessDir = require('./getServerlessDir');
const readFileSync = require('./fs/readFileSync');
const fileExistsSync = require('./fs/fileExistsSync');
function getFrameworkId() {
const serverlessHomePath = getServerlessDir();
const statsEnabledFilePath = path.join(serverlessHomePath, 'stats-enabled');
const statsDisabledFilePath = path.join(serverlessHomePath, 'stats-disabled');
const serverlessRCFilePath = path.join(serverlessHomePath, '.serverlessrc');
if (fileExistsSync(statsEnabledFilePath)) {
return readFileSync(statsEnabledFilePath).toString();
}
if (fileExistsSync(statsDisabledFilePath)) {
return readFileSync(statsDisabledFilePath).toString();
}
if (fileExistsSync(serverlessRCFilePath)) {
const config = JSON.parse(readFileSync(serverlessRCFilePath));
if (config && config.frameworkId) {
return config.frameworkId;
}
}
return null;
}
module.exports = getFrameworkId;

View File

@ -0,0 +1,11 @@
'use strict';
const path = require('path');
const os = require('os');
// get .serverless home path
function getServerlessDir() {
return path.join(os.homedir(), '.serverless');
}
module.exports = getServerlessDir;

View File

@ -0,0 +1,20 @@
'use strict';
const path = require('path');
const readFileSync = require('./fs/readFileSync');
const fileExistsSync = require('./fs/fileExistsSync');
/* Check if is inside docker container */
module.exports = function isDockerContainer() {
// wrap in try catch to make sure that missing permissions won't break anything
try {
const cgroupFilePath = path.join('/', 'proc', '1', 'cgroup');
if (fileExistsSync(cgroupFilePath)) {
const cgroupFileContent = readFileSync(cgroupFilePath).toString();
return !!cgroupFileContent.match(/docker/);
}
} catch (exception) {
// do nothing
}
return false;
};

View File

@ -0,0 +1,13 @@
'use strict';
const path = require('path');
const fileExistsSync = require('./fs/fileExistsSync');
const getServerlessDir = require('./getServerlessDir');
module.exports = function isTrackingDisabled() {
// to be updated to .serverlessrc
if (fileExistsSync(path.join(getServerlessDir(), 'stats-disabled'))) {
return true;
}
return false;
};

34
lib/utils/openBrowser.js Normal file
View File

@ -0,0 +1,34 @@
'use strict';
/* eslint-disable no-console */
const opn = require('opn');
const chalk = require('chalk');
const isDockerContainer = require('./isDockerContainer');
function displayManualOpenMessage(url) {
// https://github.com/sindresorhus/log-symbols
console.log('---------------------------');
console.log(`🙈 ${chalk.red('Unable to open browser automatically')}`);
console.log(chalk.green('Please open your browser & open the URL below to login:'));
console.log(chalk.yellow(url));
console.log('---------------------------');
return false;
}
module.exports = function openBrowser(url) {
let browser = process.env.BROWSER;
if (browser === 'none' || isDockerContainer()) {
return displayManualOpenMessage(url);
}
if (process.platform === 'darwin' && browser === 'open') {
browser = undefined;
}
try {
const options = { app: browser };
opn(url, options).catch(() => {});
return true;
} catch (err) {
return displayManualOpenMessage(url);
}
};

37
lib/utils/segment.js Normal file
View File

@ -0,0 +1,37 @@
'use strict';
const BbPromise = require('bluebird');
const fetch = require('node-fetch');
const isTrackingDisabled = require('./isTrackingDisabled');
// TODO: Replace me before release
const writeKey = 'XXXX';
const auth = `${writeKey}:`;
const TRACKING_IS_DISABLED = isTrackingDisabled();
/* note segment call swallows errors */
function request(url, payload) {
return fetch(url, {
headers: {
Authorization: `Basic ${new Buffer(auth).toString('base64')}`,
'content-type': 'application/json',
},
method: 'POST',
timeout: '1000',
body: JSON.stringify(payload),
})
.then((response) => response.json())
.then(() => BbPromise.resolve())
.catch(() => BbPromise.resolve());
}
function track(payload) {
// exit early is tracking disabled
if (TRACKING_IS_DISABLED) {
return BbPromise.resolve();
}
return request('https://api.segment.io/v1/track', payload);
}
module.exports = {
track,
};

111
lib/utils/userStats.js Normal file
View File

@ -0,0 +1,111 @@
'use strict';
/* eslint-disable no-console */
const BbPromise = require('bluebird');
const _ = require('lodash');
const fetch = require('node-fetch');
const configUtils = require('./config');
const isTrackingDisabled = require('./isTrackingDisabled');
const isValidEventName = require('./userStatsValidation');
const TRACKING_IS_DISABLED = isTrackingDisabled();
const TRACK_URL = 'https://serverless.com/api/framework/track';
const IDENTIFY_URL = 'https://serverless.com/api/framework/identify';
const DEBUG = false;
function debug() {
if (DEBUG) console.log(arguments);
}
/* note tracking swallows errors */
function request(url, payload) {
return fetch(url, {
method: 'POST',
// set to 1000 b/c no response needed
timeout: '1000',
body: JSON.stringify(payload),
})
.then((response) => {
if (response.status === 404) {
return BbPromise.reject('404 api not found');
}
return response.json();
})
.then((res) => BbPromise.resolve(res))
.catch((e) => BbPromise.resolve(e));
}
function track(eventName, payload) {
const data = payload || {};
let userId = data.id;
let userEmail = data.email;
// exit early if tracking disabled
if (TRACKING_IS_DISABLED && !data.force) {
debug('abort .track call TRACKING_IS_DISABLED');
return BbPromise.resolve();
}
// getConfig for values if not provided from .track call
if (!userId || !userEmail) {
const config = configUtils.getConfig();
userId = config.userId;
if (config.users && config.users[userId] && config.users[userId].email) {
userEmail = config.users[userId].email;
} else {
debug('exit early if no user data or email found');
return BbPromise.resolve();
}
}
// automatically add `framework:` prefix
if (eventName.indexOf('framework:') === -1) {
eventName = `framework:${eventName}`; // eslint-disable-line
}
// to ensure clean data, validate event name
if (!isValidEventName(eventName)) {
return BbPromise.resolve();
}
const defaultData = {
event: eventName,
id: userId,
email: userEmail,
data: {
id: userId,
timestamp: Math.round(+new Date() / 1000),
},
};
delete data.force;
const eventData = _.merge(defaultData, data);
if (DEBUG) {
debug('.track call', eventData);
return BbPromise.resolve();
}
return request(TRACK_URL, eventData);
}
function identify(payload) {
const data = payload || {};
if (TRACKING_IS_DISABLED && !data.force) {
if (DEBUG) {
console.log('abort .identify call');
}
// exit early is tracking disabled
return BbPromise.resolve();
}
delete data.force;
if (DEBUG) {
console.log('.identify call', data);
return BbPromise.resolve();
}
return request(IDENTIFY_URL, data);
}
module.exports = {
track,
identify,
};

View File

@ -0,0 +1,85 @@
'use strict';
/* eslint-disable no-console */
/**
* Validate Event names to keep data clean
*/
const chalk = require('chalk');
const VALID_TRACKING_PROJECTS = ['framework'];
const VALID_TRACKING_OBJECTS = ['user', 'service'];
function containsSeparators(eventName) {
const underscores = (eventName.match(/_/g) || []).length;
const colons = (eventName.match(/:/g) || []).length;
if (underscores !== 1) {
console.log(chalk.red('Tracking Error:'));
console.log(
chalk.red(`Event name must have single underscore. "${eventName}" contains ${underscores}`));
return false;
}
if (colons !== 1) {
console.log(chalk.red('Tracking Error:'));
console.log(chalk.red(`Event name must have single colon. "${eventName}" contains ${colons}`));
return false;
}
return true;
}
// Validate tracking project for clean events
function isValidProject(project) {
const isValid = VALID_TRACKING_PROJECTS.indexOf(project) !== -1;
if (!isValid) {
console.log(chalk.red('Tracking Error:'));
console.log(`"${project}" is invalid project. Must be one of`, VALID_TRACKING_PROJECTS);
}
return isValid;
}
// Validate tracking objects for clean events
function isValidObject(key) {
const isValid = VALID_TRACKING_OBJECTS.indexOf(key) !== -1;
if (!isValid) {
console.log(chalk.red('Tracking Error:'));
console.log(`"${key}" is invalid tracking object. Must be one of`, VALID_TRACKING_OBJECTS);
}
return isValid;
}
function formattingWarning(eventName) {
console.log(chalk.red(`Incorrect tracking event format: "${eventName}"`));
console.log(`Tracking event must match ${chalk.yellow('product:objectName_actionName')}`);
console.log(`It must be all camelCase: ${chalk.yellow('camelCase:camelCase_camelCase')}`);
console.log(`Here is an Example ${chalk.green('framework:user_loggedIn')}`);
console.log('Note: "framework:" is automatically prepended');
console.log(`Usage: ${chalk.yellow('track("user_loggedIn", { ..extraData });')}`);
console.log('-----------------------------');
return false;
}
// validate events to naming conventions. clean data FTW!
module.exports = function isValidEventName(eventName) {
// match framework:objectName_actionName
const matches = eventName.match(/([a-zA-Z]*):([a-zA-Z]*)_(.*)/);
if (!containsSeparators(eventName) || !matches) {
return formattingWarning(eventName);
}
const project = matches[1];
const object = matches[2];
const action = matches[3];
// if missing any parts of event, exit;
if (!project || !object || !action) {
return formattingWarning(eventName);
}
// validate project name
if (!isValidProject(project)) {
return formattingWarning(eventName);
}
// validate object name
if (!isValidObject(object)) {
return formattingWarning(eventName);
}
return true;
};

613
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -98,15 +98,19 @@
"https-proxy-agent": "^1.0.0",
"js-yaml": "^3.6.1",
"json-refs": "^2.1.5",
"jwt-decode": "^2.2.0",
"lodash": "^4.13.1",
"minimist": "^1.2.0",
"moment": "^2.13.0",
"node-fetch": "^1.6.0",
"opn": "^5.0.0",
"rc": "^1.1.6",
"replaceall": "^0.1.6",
"resolve-from": "^2.0.0",
"semver": "^5.0.3",
"semver-regex": "^1.0.0",
"shelljs": "^0.6.0",
"uuid": "^2.0.2"
"uuid": "^2.0.2",
"write-file-atomic": "^2.1.0"
}
}