"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getInquirerDefault = exports.promptCreateSecret = exports.askForParam = exports.ask = exports.checkResponse = exports.SecretLocation = void 0; const _ = require("lodash"); const clc = require("colorette"); const { marked } = require("marked"); const types_1 = require("./types"); const secretManagerApi = require("../gcp/secretManager"); const secretsUtils = require("./secretsUtils"); const extensionsHelper_1 = require("./extensionsHelper"); const utils_1 = require("./utils"); const logger_1 = require("../logger"); const prompt_1 = require("../prompt"); const utils = require("../utils"); const projectUtils_1 = require("../projectUtils"); var SecretLocation; (function (SecretLocation) { SecretLocation[SecretLocation["CLOUD"] = 1] = "CLOUD"; SecretLocation[SecretLocation["LOCAL"] = 2] = "LOCAL"; })(SecretLocation = exports.SecretLocation || (exports.SecretLocation = {})); var SecretUpdateAction; (function (SecretUpdateAction) { SecretUpdateAction[SecretUpdateAction["LEAVE"] = 1] = "LEAVE"; SecretUpdateAction[SecretUpdateAction["SET_NEW"] = 2] = "SET_NEW"; })(SecretUpdateAction || (SecretUpdateAction = {})); function checkResponse(response, spec) { var _a; let valid = true; let responses; if (spec.required && (response === "" || response === undefined)) { utils.logWarning(`Param ${spec.param} is required, but no value was provided.`); return false; } if (spec.type === types_1.ParamType.MULTISELECT) { responses = response.split(","); } else { responses = [response]; } if (spec.validationRegex && !!response) { const re = new RegExp(spec.validationRegex); for (const resp of responses) { if ((spec.required || resp !== "") && !re.test(resp)) { const genericWarn = `${resp} is not a valid value for ${spec.param} since it` + ` does not meet the requirements of the regex validation: "${spec.validationRegex}"`; utils.logWarning(spec.validationErrorMessage || genericWarn); valid = false; } } } if (spec.type && (spec.type === types_1.ParamType.MULTISELECT || spec.type === types_1.ParamType.SELECT)) { for (const r of responses) { const validChoice = (_a = spec.options) === null || _a === void 0 ? void 0 : _a.some((option) => r === option.value); if (r && !validChoice) { utils.logWarning(`${r} is not a valid option for ${spec.param}.`); valid = false; } } } return valid; } exports.checkResponse = checkResponse; async function ask(args) { if (_.isEmpty(args.paramSpecs)) { logger_1.logger.debug("No params were specified for this extension."); return {}; } utils.logLabeledBullet(extensionsHelper_1.logPrefix, "answer the questions below to configure your extension:"); const substituted = (0, extensionsHelper_1.substituteParams)(args.paramSpecs, args.firebaseProjectParams); const result = {}; const promises = substituted.map((paramSpec) => { return async () => { result[paramSpec.param] = await askForParam({ projectId: args.projectId, instanceId: args.instanceId, paramSpec: paramSpec, reconfiguring: args.reconfiguring, }); }; }); await promises.reduce((prev, cur) => prev.then(cur), Promise.resolve()); logger_1.logger.info(); return result; } exports.ask = ask; async function askForParam(args) { const paramSpec = args.paramSpec; let valid = false; let response = ""; let responseForLocal; let secretLocations = []; const description = paramSpec.description || ""; const label = paramSpec.label.trim(); logger_1.logger.info(`\n${clc.bold(label)}${clc.bold(paramSpec.required ? "" : " (Optional)")}: ${marked(description).trim()}`); while (!valid) { switch (paramSpec.type) { case types_1.ParamType.SELECT: response = await (0, prompt_1.promptOnce)({ name: "input", type: "list", default: () => { if (paramSpec.default) { return getInquirerDefault(_.get(paramSpec, "options", []), paramSpec.default); } }, message: "Which option do you want enabled for this parameter? " + "Select an option with the arrow keys, and use Enter to confirm your choice. " + "You may only select one option.", choices: (0, utils_1.convertExtensionOptionToLabeledList)(paramSpec.options), }); valid = checkResponse(response, paramSpec); break; case types_1.ParamType.MULTISELECT: response = await (0, utils_1.onceWithJoin)({ name: "input", type: "checkbox", default: () => { if (paramSpec.default) { const defaults = paramSpec.default.split(","); return defaults.map((def) => { return getInquirerDefault(_.get(paramSpec, "options", []), def); }); } }, message: "Which options do you want enabled for this parameter? " + "Press Space to select, then Enter to confirm your choices. ", choices: (0, utils_1.convertExtensionOptionToLabeledList)(paramSpec.options), }); valid = checkResponse(response, paramSpec); break; case types_1.ParamType.SECRET: do { secretLocations = await promptSecretLocations(paramSpec); } while (!isValidSecretLocations(secretLocations, paramSpec)); if (secretLocations.includes(SecretLocation.CLOUD.toString())) { const projectId = (0, projectUtils_1.needProjectId)({ projectId: args.projectId }); response = args.reconfiguring ? await promptReconfigureSecret(projectId, args.instanceId, paramSpec) : await promptCreateSecret(projectId, args.instanceId, paramSpec); } if (secretLocations.includes(SecretLocation.LOCAL.toString())) { responseForLocal = await promptLocalSecret(args.instanceId, paramSpec); } valid = true; break; default: response = await (0, prompt_1.promptOnce)({ name: paramSpec.param, type: "input", default: paramSpec.default, message: `Enter a value for ${label}:`, }); valid = checkResponse(response, paramSpec); } } return Object.assign({ baseValue: response }, (responseForLocal ? { local: responseForLocal } : {})); } exports.askForParam = askForParam; function isValidSecretLocations(secretLocations, paramSpec) { if (paramSpec.required) { return !!secretLocations.length; } return true; } async function promptSecretLocations(paramSpec) { if (paramSpec.required) { return await (0, prompt_1.promptOnce)({ name: "input", type: "checkbox", message: "Where would you like to store your secrets? You must select at least one value", choices: [ { checked: true, name: "Google Cloud Secret Manager (Used by deployed extensions and emulator)", value: SecretLocation.CLOUD.toString(), }, { checked: false, name: "Local file (Used by emulator only)", value: SecretLocation.LOCAL.toString(), }, ], }); } return await (0, prompt_1.promptOnce)({ name: "input", type: "checkbox", message: "Where would you like to store your secrets? " + "If you don't want to set this optional secret, leave both options unselected to skip it", choices: [ { checked: false, name: "Google Cloud Secret Manager (Used by deployed extensions and emulator)", value: SecretLocation.CLOUD.toString(), }, { checked: false, name: "Local file (Used by emulator only)", value: SecretLocation.LOCAL.toString(), }, ], }); } async function promptLocalSecret(instanceId, paramSpec) { let value; do { utils.logLabeledBullet(extensionsHelper_1.logPrefix, "Configure a local secret value for Extensions Emulator"); value = await (0, prompt_1.promptOnce)({ name: paramSpec.param, type: "input", message: `This secret will be stored in ./extensions/${instanceId}.secret.local.\n` + `Enter value for "${paramSpec.label.trim()}" to be used by Extensions Emulator:`, }); } while (!value); return value; } async function promptReconfigureSecret(projectId, instanceId, paramSpec) { const action = await (0, prompt_1.promptOnce)({ type: "list", message: `Choose what you would like to do with this secret:`, choices: [ { name: "Leave unchanged", value: SecretUpdateAction.LEAVE }, { name: "Set new value", value: SecretUpdateAction.SET_NEW }, ], }); switch (action) { case SecretUpdateAction.SET_NEW: let secret; let secretName; if (paramSpec.default) { secret = secretManagerApi.parseSecretResourceName(paramSpec.default); secretName = secret.name; } else { secretName = await generateSecretName(projectId, instanceId, paramSpec.param); } const secretValue = await (0, prompt_1.promptOnce)({ name: paramSpec.param, type: "password", message: `This secret will be stored in Cloud Secret Manager as ${secretName}.\nEnter new value for ${paramSpec.label.trim()}:`, }); if (secretValue === "" && paramSpec.required) { logger_1.logger.info(`Secret value cannot be empty for required param ${paramSpec.param}`); return promptReconfigureSecret(projectId, instanceId, paramSpec); } else if (secretValue !== "") { if (checkResponse(secretValue, paramSpec)) { if (!secret) { secret = await secretManagerApi.createSecret(projectId, secretName, secretsUtils.getSecretLabels(instanceId)); } return addNewSecretVersion(projectId, instanceId, secret, paramSpec, secretValue); } else { return promptReconfigureSecret(projectId, instanceId, paramSpec); } } else { return ""; } case SecretUpdateAction.LEAVE: default: return paramSpec.default || ""; } } async function promptCreateSecret(projectId, instanceId, paramSpec, secretName) { const name = secretName !== null && secretName !== void 0 ? secretName : (await generateSecretName(projectId, instanceId, paramSpec.param)); const secretValue = await (0, prompt_1.promptOnce)({ name: paramSpec.param, type: "password", default: paramSpec.default, message: `This secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing) as ${name} and managed by Firebase Extensions (Firebase Extensions Service Agent will be granted Secret Admin role on this secret).\nEnter a value for ${paramSpec.label.trim()}:`, }); if (secretValue === "" && paramSpec.required) { logger_1.logger.info(`Secret value cannot be empty for required param ${paramSpec.param}`); return promptCreateSecret(projectId, instanceId, paramSpec, name); } else if (secretValue !== "") { if (checkResponse(secretValue, paramSpec)) { const secret = await secretManagerApi.createSecret(projectId, name, secretsUtils.getSecretLabels(instanceId)); return addNewSecretVersion(projectId, instanceId, secret, paramSpec, secretValue); } else { return promptCreateSecret(projectId, instanceId, paramSpec, name); } } else { return ""; } } exports.promptCreateSecret = promptCreateSecret; async function generateSecretName(projectId, instanceId, paramName) { let secretName = `ext-${instanceId}-${paramName}`; while (await secretManagerApi.secretExists(projectId, secretName)) { secretName += `-${(0, utils_1.getRandomString)(3)}`; } return secretName; } async function addNewSecretVersion(projectId, instanceId, secret, paramSpec, secretValue) { const version = await secretManagerApi.addVersion(projectId, secret.name, secretValue); await secretsUtils.grantFirexServiceAgentSecretAdminRole(secret); return `projects/${version.secret.projectId}/secrets/${version.secret.name}/versions/${version.versionId}`; } function getInquirerDefault(options, def) { const defaultOption = options.find((o) => o.value === def); return defaultOption ? defaultOption.label || defaultOption.value : ""; } exports.getInquirerDefault = getInquirerDefault;