ask-cli
Version:
Alexa Skills Kit (ASK) Command Line Interfaces
252 lines (235 loc) • 10.8 kB
JavaScript
const fs = require('fs');
const open = require('open');
const path = require('path');
const portScanner = require('portscanner');
const { URL, URLSearchParams } = require('url');
const CONSTANTS = require('@src/utils/constants');
const LocalHostServer = require('@src/utils/local-host-server');
const stringUtils = require('@src/utils/string-utils');
const Messenger = require('@src/view/messenger');
const SpinnerView = require('@src/view/spinner-view');
const SkillMetadataController = require('@src/controllers/skill-metadata-controller');
const permissionStatusMap = new Map();
permissionStatusMap.set(CONSTANTS.HOSTED_SKILL.PERMISSION_CHECK_RESULT.NEW_USER_REGISTRATION_REQUIRED,
'Failed to validate CAPTCHA. Please try again.');
permissionStatusMap.set(CONSTANTS.HOSTED_SKILL.PERMISSION_CHECK_RESULT.RESOURCE_LIMIT_EXCEEDED,
'Your hosted skills account is limited to a certain number of new hosted skills per minute. '
+ 'Please try again later.');
permissionStatusMap.set(CONSTANTS.HOSTED_SKILL.PERMISSION_CHECK_RESULT.ALLOWED, null);
module.exports = {
validateUserQualification,
createHostedSkill
};
/**
* To manage check permission and captcha validation for first time user
* @param {string} vendorId The vendor ID
* @param {Object} hostedSkillController The controller containing hosted skill main functionalities
* @param {callback} callback { error, response }
*/
function validateUserQualification(vendorId, hostedSkillController, callback) {
_checkPermission(hostedSkillController, vendorId, false, (error, actionUrl) => {
if (error) {
return callback(error);
}
if (!actionUrl) {
return callback();
}
_solveCaptcha(vendorId, actionUrl, (captchaValidateError) => {
if (captchaValidateError) {
return callback(captchaValidateError);
}
// confirm captcha has been solved
_checkPermission(hostedSkillController, vendorId, true, (reCheckPermissionError) => {
if (reCheckPermissionError) {
return callback(reCheckPermissionError);
}
Messenger.getInstance().info('CAPTCHA validation was successfully completed. You are able to create a Alexa hosted skill.');
return callback();
});
});
});
}
/**
* To create an Alexa hosted skill and clone it into local machine
* @param {Object} hostedSkillController The controller containing hosted skill main functionalities
* @param {Object} userInput The user input { deploymentType, language, projectFolderName, skillName }
* @param {string} vendorId The vendor ID
* @param {callback} callback { error, response }
*/
function createHostedSkill(hostedSkillController, userInput, vendorId, callback) {
const rootPath = process.cwd();
const projectPath = path.join(rootPath, userInput.projectFolderName);
const { profile, doDebug } = hostedSkillController;
const skillMetadataController = new SkillMetadataController({ profile, doDebug });
if (fs.existsSync(projectPath)) {
return callback(`${projectPath} directory already exists.`);
}
const manifest = _updateManifest(userInput.skillName);
const input = {
vendorId,
manifest,
runtime: userInput.language
};
const listenSpinner = new SpinnerView();
listenSpinner.start('Creating your Alexa hosted skill. It will take about a minute.');
hostedSkillController.createSkill(input, (createErr, skillId) => {
listenSpinner.terminate();
if (createErr) {
if (!skillId) {
return callback(createErr);
}
hostedSkillController.deleteSkill(skillId, callback(createErr));
}
hostedSkillController.clone(skillId, userInput.skillName, projectPath, (cloneErr) => {
if (cloneErr) {
return callback(cloneErr);
}
skillMetadataController.enableSkill((enableErr) => {
if (enableErr) {
return callback(enableErr);
}
const templateUrl = CONSTANTS.HOSTED_SKILL.GIT_HOOKS_TEMPLATES.PRE_PUSH.URL;
const filePath = `${userInput.projectFolderName}/.git/hooks/pre-push`;
hostedSkillController.downloadGitHooksTemplate(templateUrl, filePath, (hooksErr) => {
if (hooksErr) {
return callback(hooksErr);
}
callback(null, skillId);
});
});
});
});
}
/**
* To update hosted skill manifest template with customer input skill name
* and return the manifest object.
* @param {string} skillName The skill name
*/
function _updateManifest(skillName) {
const manifest = CONSTANTS.HOSTED_SKILL.MANIFEST;
const { locales } = manifest.publishingInformation;
Object.keys(locales).forEach((locale) => {
manifest.publishingInformation.locales[locale].name = skillName;
});
return manifest;
}
/**
* Check user's permission to access hosted skill and handle error messages
* @param {Object} hostedSkillController The controller containing hosted skill main functionalities
* @param {boolean} isReCheck the recheck flag
* @param {callback} callback { error, response }
*/
function _checkPermission(hostedSkillController, vendorId, isReCheck, callback) {
hostedSkillController.getHostedSkillPermission(vendorId, CONSTANTS.HOSTED_SKILL.PERMISSION_ENUM.NEW_SKILL,
(permissionError, permissionRes) => {
if (permissionError) {
return callback(permissionError);
}
if (!isReCheck
&& permissionRes.status === CONSTANTS.HOSTED_SKILL.PERMISSION_CHECK_RESULT.NEW_USER_REGISTRATION_REQUIRED) {
Messenger.getInstance().info('CAPTCHA validation is required for a new hosted skill user.');
return callback(null, permissionRes.actionUrl);
}
const errorMessage = permissionStatusMap.get(permissionRes.status);
if (errorMessage) {
return callback(errorMessage);
}
return callback();
});
}
/**
* To navigate user to the captcha validation page
* @param {string} vendorId The Vendor ID
* @param {string} captchaUrl The captcha url
* @param {callback} callback { error, response }
*/
function _solveCaptcha(vendorId, captchaUrl, callback) {
Messenger.getInstance().info('Go to the CAPTCHA page, confirm that you are signed into the correct developer account, and solve the CAPTCHA.\n'
+ 'If your browser does not open the page, quit this process, paste the following url into your browser, '
+ `and complete the CAPTCHA.\n${captchaUrl}`);
portScanner.checkPortStatus(CONSTANTS.LOCALHOST_PORT, (err, status) => {
if (err) {
callback(err);
} else {
if (status === 'closed') {
_openLoginUrlWithRedirectLink(captchaUrl, vendorId);
_listenResponseFromCaptchaServer(CONSTANTS.LOCALHOST_PORT, (error) => {
callback(error);
});
} else {
callback(`${CONSTANTS.LOCALHOST_PORT} port on localhost has been occupied, `
+ 'ask-cli cannot start a local server for receiving authorization code.\n'
+ `Please either abort any processes running on port ${CONSTANTS.LOCALHOST_PORT} `
+ 'or add `--no-browser` flag to the command as an alternative approach.');
}
}
});
}
/**
* Build and open the url that navigates user to captcha validation page after login.
* @param captcha the validation url
* @param vendorId the vendor Id
*/
function _openLoginUrlWithRedirectLink(captchaUrl, vendorId) {
const envVarSignInHost = process.env.ASK_LWA_AUTHORIZE_HOST;
const loginUrl = new URL(envVarSignInHost && stringUtils.isNonBlankString(envVarSignInHost)
? envVarSignInHost + CONSTANTS.HOSTED_SKILL.SIGNIN_PATH : CONSTANTS.LWA.SIGNIN_URL);
loginUrl.search = new URLSearchParams([
['openid.ns', 'http://specs.openid.net/auth/2.0'],
['openid.mode', 'checkid_setup'],
['openid.claimed_id', 'http://specs.openid.net/auth/2.0/identifier_select'],
['openid.identity', 'http://specs.openid.net/auth/2.0/identifier_select'],
['openid.assoc_handle', 'amzn_dante_us'],
['openid.return_to', `${captchaUrl}?vendor_id=${vendorId}&redirect_url=http://127.0.0.1:9090/captcha`],
['openid.pape.max_auth_age', 7200]
]).toString();
open(loginUrl.href);
}
/**
* Start a local server and listen the response from the captcha validation server,
* then extract validation result from it.
* @param PORT
* @param callback with error
*/
function _listenResponseFromCaptchaServer(PORT, callback) {
const listenSpinner = new SpinnerView();
const server = new LocalHostServer(PORT);
server.create(handleServerRequest);
server.listen(() => {
listenSpinner.start(` Listening on http://localhost:${PORT}...`);
});
server.registerEvent('connection', (socket) => {
socket.unref();
});
function handleServerRequest(request, response) {
response.on('close', () => {
request.socket.destroy();
});
listenSpinner.terminate();
server.destroy();
if (request.url.startsWith('/captcha?success')) {
response.end('CAPTCHA validation was successful. Please close the browser and return to the command line interface.');
callback();
} else if (request.url.startsWith('/captcha?error')) {
const errorMsg = 'Failed to validate the CAPTCHA with internal service error. Please try again later.';
response.statusCode = 500;
response.end(errorMsg);
callback(errorMsg);
} else if (request.url.startsWith('/captcha?vendorId')) {
const errorMsg = 'The Vendor ID in the browser session does not match the one associated with your CLI profile. \n'
+ 'Please sign into the correct developer account in your browser before completing the CAPTCHA.';
response.statusCode = 400;
response.end(errorMsg);
callback(errorMsg);
} else if (request.url.startsWith('/favicon.ico')) {
request.socket.destroy();
response.statusCode = 204;
response.end();
} else {
const errorMsg = 'Failed to validate the CAPTCHA. Please try again.';
response.statusCode = 404;
response.end(errorMsg);
callback(errorMsg);
}
}
}