"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UseExistingPushNotification = exports.UpdateIosPush = exports.RemoveIosPush = exports.CreateOrReusePushKey = exports.CreateIosPush = exports.CreateAndAssignIosPush = void 0; exports.getPushKeyFromParams = getPushKeyFromParams; exports.usePushKeyFromParams = usePushKeyFromParams; exports.validatePushKey = validatePushKey; function _chalk() { const data = _interopRequireDefault(require("chalk")); _chalk = function () { return data; }; return data; } function _fsExtra() { const data = _interopRequireDefault(require("fs-extra")); _fsExtra = function () { return data; }; return data; } function _terminalLink() { const data = _interopRequireDefault(require("terminal-link")); _terminalLink = function () { return data; }; return data; } function _CommandError() { const data = _interopRequireDefault(require("../../CommandError")); _CommandError = function () { return data; }; return data; } function _appleApi() { const data = require("../../appleApi"); _appleApi = function () { return data; }; return data; } function _log() { const data = _interopRequireDefault(require("../../log")); _log = function () { return data; }; return data; } function _ora() { const data = require("../../utils/ora"); _ora = function () { return data; }; return data; } function _prompts() { const data = _interopRequireWildcard(require("../../utils/prompts")); _prompts = function () { return data; }; return data; } function _list() { const data = require("../actions/list"); _list = function () { return data; }; return data; } function _promptForCredentials() { const data = require("../actions/promptForCredentials"); _promptForCredentials = function () { return data; }; return data; } function _IosApi() { const data = require("../api/IosApi"); _IosApi = function () { return data; }; return data; } function _credentials() { const data = require("../credentials"); _credentials = function () { return data; }; return data; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const APPLE_KEYS_TOO_MANY_GENERATED_ERROR = ` You can have only ${_chalk().default.underline('two')} Push Notifactions Keys on your Apple Developer account. Please revoke the old ones or reuse existing from your other apps. Please remember that Apple Keys are not application specific! `; class CreateIosPush { constructor(accountName) { this.accountName = accountName; } async create(ctx) { const newPushKey = await this.provideOrGenerate(ctx); return await ctx.ios.createPushKey(this.accountName, newPushKey); } async open(ctx) { const pushKey = await this.create(ctx); _log().default.log('Successfully created Push Notification Key\n'); (0, _list().displayIosUserCredentials)(pushKey); _log().default.log(); return null; } _getRequiredQuestions(ctx) { const requiredQuestions = { ..._credentials().pushKeySchema }; if (ctx.hasAppleCtx() && requiredQuestions.questions) { requiredQuestions.required = requiredQuestions.required.filter(q => q !== 'teamId'); } return requiredQuestions; } _ensurePushKey(ctx, partialKey) { if (ctx.hasAppleCtx()) { partialKey.teamId = ctx.appleCtx.team.id; partialKey.teamName = ctx.appleCtx.team.name; } if (!(0, _appleApi().isPushKey)(partialKey)) { throw new Error(`Not of type PushKey: ${partialKey}`); } return partialKey; } async provideOrGenerate(ctx) { if (!ctx.nonInteractive) { const requiredQuestions = this._getRequiredQuestions(ctx); const userProvided = await (0, _promptForCredentials().askForUserProvided)(requiredQuestions); if (userProvided) { const pushKey = this._ensurePushKey(ctx, userProvided); const isValid = await validatePushKey(ctx, pushKey); return isValid ? userProvided : await this.provideOrGenerate(ctx); } } return await generatePushKey(ctx, this.accountName); } } exports.CreateIosPush = CreateIosPush; class CreateAndAssignIosPush extends CreateIosPush { async open(ctx) { const pushKey = await super.create(ctx); _log().default.log('Successfully created Push Notification Key\n'); (0, _list().displayIosUserCredentials)(pushKey); _log().default.log(); if (ctx.hasProjectContext && pushKey) { await this.assignToCurrentProject(ctx, pushKey.id); _log().default.log(); } return null; } async assignToCurrentProject(ctx, pushKeyId) { var _ctx$manifest, _ctx$manifest$ios; const experienceName = `@${ctx.projectOwner}/${ctx.manifest.slug}`; const bundleIdentifier = (_ctx$manifest = ctx.manifest) === null || _ctx$manifest === void 0 ? void 0 : (_ctx$manifest$ios = _ctx$manifest.ios) === null || _ctx$manifest$ios === void 0 ? void 0 : _ctx$manifest$ios.bundleIdentifier; if (!ctx.nonInteractive && bundleIdentifier) { const confirm = await (0, _prompts().confirmAsync)({ message: `Would you like to use this push key for the current project: ${experienceName} (${bundleIdentifier})?` }); if (!confirm) { return; } const app = (0, _IosApi().getAppLookupParams)(experienceName, bundleIdentifier); await ctx.ios.usePushKey(app, pushKeyId); _log().default.log(_chalk().default.green(`Successfully assigned Push Key to ${experienceName} (${bundleIdentifier})`)); } } } exports.CreateAndAssignIosPush = CreateAndAssignIosPush; class RemoveIosPush { constructor(accountName, shouldRevoke = false) { this.accountName = accountName; this.shouldRevoke = shouldRevoke; } async open(ctx) { if (ctx.nonInteractive) { throw new (_CommandError().default)('NON_INTERACTIVE', "Start the CLI without the '--non-interactive' flag to select a push notification credential to remove."); } const selected = await selectPushCredFromList(ctx, this.accountName); if (selected) { if (!('type' in selected)) { const app = (0, _IosApi().getAppLookupParams)(selected.experienceName, selected.bundleIdentifier); await this.removePushCert(ctx, app); _log().default.log(_chalk().default.green('Successfully removed Push Certificate')); } else { await this.removeSpecific(ctx, selected); _log().default.log(_chalk().default.green('Successfully removed Push Notification Key')); } } return null; } async removePushCert(ctx, app) { _log().default.log('Removing Push Certificate'); await ctx.ios.deletePushCert(app); } async removeSpecific(ctx, selected) { const credentials = await ctx.ios.getAllCredentials(this.accountName); const apps = getAppsUsingPushCred(credentials, selected); const appsList = apps.map(appCred => appCred.experienceName).join(', '); if (appsList && !ctx.nonInteractive) { _log().default.log('Removing Push Key'); const confirm = await (0, _prompts().confirmAsync)({ message: `Removing this key/cert will disable notifications in ${appsList}. Do you want to continue?` }); if (!confirm) { _log().default.log('Aborting'); return; } } _log().default.log('Removing Push Key...\n'); await ctx.ios.deletePushKey(selected.id, this.accountName); let shouldRevoke = this.shouldRevoke; if (!shouldRevoke && !ctx.nonInteractive) { const revoke = await (0, _prompts().confirmAsync)({ message: `Do you also want to revoke it on Apple Developer Portal?` }); shouldRevoke = revoke; } if (shouldRevoke) { await ctx.ensureAppleCtx(); await new (_appleApi().PushKeyManager)(ctx.appleCtx).revoke([selected.apnsKeyId]); } } } exports.RemoveIosPush = RemoveIosPush; class UpdateIosPush { constructor(accountName) { this.accountName = accountName; } async open(ctx) { if (ctx.nonInteractive) { throw new (_CommandError().default)('NON_INTERACTIVE', "Start the CLI without the '--non-interactive' flag to select a push notification credential to update."); } const selected = await selectPushCredFromList(ctx, this.accountName, { allowLegacy: false }); if (selected) { await this.updateSpecific(ctx, selected); _log().default.log(_chalk().default.green('Successfully updated Push Notification Key.\n')); const credentials = await ctx.ios.getAllCredentials(this.accountName); const updated = credentials.userCredentials.find(i => i.id === selected.id); if (updated) { (0, _list().displayIosUserCredentials)(updated); } _log().default.log(); } return null; } async updateSpecific(ctx, selected) { const credentials = await ctx.ios.getAllCredentials(this.accountName); const apps = getAppsUsingPushCred(credentials, selected); const appsList = apps.map(appCred => appCred.experienceName).join(', '); if (apps.length > 1) { if (ctx.nonInteractive) { throw new (_CommandError().default)('NON_INTERACTIVE', `Updating credentials will affect all applications that are using this key (${appsList}). Start the CLI without the '--non-interactive' flag to confirm.`); } const confirm = await (0, _prompts().confirmAsync)({ message: `Update will affect all applications that are using this key (${appsList}). Do you want to continue?` }); if (!confirm) { _log().default.warn('Aborting update process'); return; } } const newPushKey = await this.provideOrGenerate(ctx); await ctx.ios.updatePushKey(selected.id, this.accountName, newPushKey); } async provideOrGenerate(ctx) { const userProvided = await (0, _promptForCredentials().askForUserProvided)(_credentials().pushKeySchema); if (userProvided) { const isValid = await validatePushKey(ctx, userProvided); return isValid ? userProvided : await this.provideOrGenerate(ctx); } return await generatePushKey(ctx, this.accountName); } } exports.UpdateIosPush = UpdateIosPush; class UseExistingPushNotification { constructor(app) { this.app = app; } async open(ctx) { if (ctx.nonInteractive) { throw new (_CommandError().default)('NON_INTERACTIVE', "Start the CLI without the '--non-interactive' flag to select a push notification credential to use."); } const selected = await selectPushCredFromList(ctx, this.app.accountName, { allowLegacy: false }); if (selected) { await ctx.ios.usePushKey(this.app, selected.id); _log().default.log(_chalk().default.green(`Successfully assigned Push Notifactions Key to ${this.app.accountName}/${this.app.projectName} (${this.app.bundleIdentifier})`)); } return null; } } exports.UseExistingPushNotification = UseExistingPushNotification; class CreateOrReusePushKey { constructor(app) { this.app = app; } async assignPushKey(ctx, userCredentialsId) { await ctx.ios.usePushKey(this.app, userCredentialsId); _log().default.log(_chalk().default.green(`Successfully assigned Push Key to ${this.app.accountName}/${this.app.projectName} (${this.app.bundleIdentifier})`)); } async open(ctx) { if (!ctx.user) { throw new Error(`This workflow requires you to be logged in.`); } const existingPushKeys = await getValidPushKeys(await ctx.ios.getAllCredentials(this.app.accountName), ctx); if (existingPushKeys.length === 0) { const pushKey = await new CreateIosPush(this.app.accountName).create(ctx); await this.assignPushKey(ctx, pushKey.id); return null; } // autoselect creds if we find valid keys const autoselectedPushKey = existingPushKeys[0]; if (!ctx.nonInteractive) { const confirm = await (0, _prompts().confirmAsync)({ message: `${formatPushKey(autoselectedPushKey, await ctx.ios.getAllCredentials(this.app.accountName), 'VALID')} \n Would you like to use this Push Key?`, limit: Infinity }); if (!confirm) { return await this._createOrReuse(ctx); } } // Use autosuggested push key _log().default.log(`Using Push Key: ${autoselectedPushKey.apnsKeyId}`); await this.assignPushKey(ctx, autoselectedPushKey.id); return null; } async _createOrReuse(ctx) { const choices = [{ title: '[Choose existing push key] (Recommended)', value: 'CHOOSE_EXISTING' }, { title: '[Add a new push key]', value: 'GENERATE' }]; const question = { type: 'select', name: 'action', message: 'Select an iOS push key to use for push notifications:', choices }; const { action } = await (0, _prompts().default)(question); if (action === 'GENERATE') { const pushKey = await new CreateIosPush(this.app.accountName).create(ctx); await this.assignPushKey(ctx, pushKey.id); return null; } else if (action === 'CHOOSE_EXISTING') { return new UseExistingPushNotification(this.app); } throw new Error('unsupported action'); } } exports.CreateOrReusePushKey = CreateOrReusePushKey; async function getValidPushKeys(iosCredentials, ctx) { const pushKeys = iosCredentials.userCredentials.filter(cred => cred.type === 'push-key'); if (!ctx.hasAppleCtx()) { _log().default.log(_chalk().default.yellow(`Unable to determine validity of Push Keys due to insufficient Apple Credentials`)); return pushKeys; } const pushKeyManager = new (_appleApi().PushKeyManager)(ctx.appleCtx); const pushInfoFromApple = await pushKeyManager.list(); return await filterRevokedPushKeys(pushInfoFromApple, pushKeys); } function getValidityStatus(pushKey, validPushKeys) { if (!validPushKeys) { return 'UNKNOWN'; } return validPushKeys.includes(pushKey) ? 'VALID' : 'INVALID'; } async function selectPushCredFromList(ctx, accountName, options = {}) { const iosCredentials = await ctx.ios.getAllCredentials(accountName); const allowLegacy = options.allowLegacy || true; let pushKeys = iosCredentials.userCredentials.filter(cred => cred.type === 'push-key'); let validPushKeys = null; if (ctx.hasAppleCtx()) { const pushKeyManager = new (_appleApi().PushKeyManager)(ctx.appleCtx); const pushInfoFromApple = await pushKeyManager.list(); validPushKeys = await filterRevokedPushKeys(pushInfoFromApple, pushKeys); } pushKeys = options.filterInvalid && validPushKeys ? validPushKeys : pushKeys; const pushCerts = allowLegacy ? iosCredentials.appCredentials.filter(({ credentials }) => credentials.pushP12 && credentials.pushPassword) : []; const pushCredentials = [...pushCerts, ...pushKeys]; if (pushCredentials.length === 0) { _log().default.warn('There are no push credentials available in your account'); return null; } const getName = pushCred => { if ('type' in pushCred) { return formatPushKey(pushCred, iosCredentials, getValidityStatus(pushCred, validPushKeys)); } const pushCert = pushCred; return `Push Certificate (PushId: ${pushCert.credentials.pushId || '------'}, TeamId: ${pushCert.credentials.teamId || '-------'} used in ${pushCert.experienceName})`; }; const question = { type: 'select', name: 'credentialsIndex', message: 'Select credentials from list', choices: pushCredentials.map((entry, index) => ({ title: getName(entry), value: index })) }; const { credentialsIndex } = await (0, _prompts().default)(question); return pushCredentials[credentialsIndex]; } function getAppsUsingPushCred(iosCredentials, pushCred) { var _pushCred$credentials, _pushCred$credentials2; if ('type' in pushCred) { return iosCredentials.appCredentials.filter(cred => cred.pushCredentialsId === pushCred.id); } else if ((_pushCred$credentials = pushCred.credentials) !== null && _pushCred$credentials !== void 0 && _pushCred$credentials.pushP12 && (_pushCred$credentials2 = pushCred.credentials) !== null && _pushCred$credentials2 !== void 0 && _pushCred$credentials2.pushPassword) { return [pushCred]; } return []; } function formatPushKeyFromApple(appleInfo, credentials) { const userCredentials = credentials.userCredentials.filter(cred => cred.type === 'push-key' && cred.apnsKeyId === appleInfo.id); const appCredentials = userCredentials.length !== 0 ? credentials.appCredentials.filter(cred => cred.pushCredentialsId === userCredentials[0].id) : []; const joinApps = appCredentials.map(i => ` ${i.experienceName} (${i.bundleIdentifier})`).join('\n'); const usedByString = joinApps ? ` ${_chalk().default.gray(`used by\n${joinApps}`)}` : ` ${_chalk().default.gray(`not used by any apps`)}`; const { name, id } = appleInfo; const pushKey = userCredentials[0]; const teamText = pushKey ? `, Team ID: ${pushKey.teamId || '---'}, Team name: ${pushKey.teamName || '---'}` : ''; return `${name} - KeyId: ${id}${teamText}\n${usedByString}`; } function formatPushKey(pushKey, credentials, validityStatus = 'UNKNOWN') { const appCredentials = credentials.appCredentials.filter(cred => cred.pushCredentialsId === pushKey.id); const joinApps = appCredentials.map(i => `${i.experienceName} (${i.bundleIdentifier})`).join(', '); const usedByString = joinApps ? `\n ${_chalk().default.gray(`used by ${joinApps}`)}` : `\n ${_chalk().default.gray(`not used by any apps`)}`; let validityText; if (validityStatus === 'VALID') { validityText = _chalk().default.gray("\n ✅ Currently valid on Apple's servers."); } else if (validityStatus === 'INVALID') { validityText = _chalk().default.gray("\n ❌ No longer valid on Apple's servers."); } else { validityText = _chalk().default.gray("\n ❓ Validity of this certificate on Apple's servers is unknown."); } return `Push Notifications Key (Key ID: ${pushKey.apnsKeyId}, Team ID: ${pushKey.teamId})${usedByString}${validityText}`; } async function generatePushKey(ctx, accountName) { await ctx.ensureAppleCtx(); const manager = new (_appleApi().PushKeyManager)(ctx.appleCtx); try { return await manager.create(); } catch (e) { if (e.code === 'APPLE_PUSH_KEYS_TOO_MANY_GENERATED_ERROR') { const keys = await manager.list(); _log().default.warn('Maximum number of Push Notifications Keys generated on Apple Developer Portal.'); _log().default.warn(APPLE_KEYS_TOO_MANY_GENERATED_ERROR); if (ctx.nonInteractive) { throw new (_CommandError().default)('NON_INTERACTIVE', "Start the CLI without the '--non-interactive' to revoke push notification keys."); } const credentials = await ctx.ios.getAllCredentials(accountName); const usedByExpo = credentials.userCredentials.filter(cert => cert.type === 'push-key').reduce((acc, cert) => ({ ...acc, [cert.apnsKeyId]: cert }), {}); // https://docs.expo.dev/distribution/app-signing/#summary const here = (0, _terminalLink().default)('here', 'https://bit.ly/3cfJJkQ'); _log().default.log(_chalk().default.grey(`⚠️ Revoking a Push Key will affect other apps that rely on it`)); _log().default.log(_chalk().default.grey(`ℹ️ Learn more ${here}`)); _log().default.log(); const { revoke } = await (0, _prompts().default)([{ type: 'multiselect', name: 'revoke', message: 'Select Push Notifications Key to revoke.', choices: keys.map((key, index) => ({ value: index, title: formatPushKeyFromApple(key, credentials) })), optionsPerPage: 20 }]); for (const index of revoke) { const certInfo = keys[index]; if (certInfo && usedByExpo[certInfo.id]) { await new RemoveIosPush(accountName, true).removeSpecific(ctx, usedByExpo[certInfo.id]); } else { await manager.revoke([certInfo.id]); } } } else { throw e; } } return await generatePushKey(ctx, accountName); } async function validatePushKey(ctx, pushKey) { if (!ctx.hasAppleCtx()) { _log().default.warn('Unable to validate Push Keys due to insufficient Apple Credentials'); return true; } const spinner = (0, _ora().ora)(`Checking validity of push key on Apple Developer Portal...`).start(); const pushKeyManager = new (_appleApi().PushKeyManager)(ctx.appleCtx); const pushInfoFromApple = await pushKeyManager.list(); const filteredFormattedPushKeyArray = await filterRevokedPushKeys(pushInfoFromApple, [pushKey]); const isValidPushKey = filteredFormattedPushKeyArray.length > 0; if (isValidPushKey) { const successMsg = `Successfully validated Push Key against Apple Servers`; spinner.succeed(successMsg); } else { const failureMsg = `This Push Key is no longer valid on the Apple Developer Portal`; spinner.fail(failureMsg); } return isValidPushKey; } async function filterRevokedPushKeys(pushInfoFromApple, pushKeys) { // if the credentials are valid, check it against apple to make sure it hasnt been revoked const validKeyIdsOnAppleServer = pushInfoFromApple.map(pushKey => pushKey.id); const validPushKeysOnExpoServer = pushKeys.filter(pushKey => { return validKeyIdsOnAppleServer.includes(pushKey.apnsKeyId); }); return validPushKeysOnExpoServer; } async function getPushKeyFromParams(builderOptions) { const { pushId, pushP8Path, teamId } = builderOptions; // none of the pushKey params were set, assume user has no intention of passing it in if (!pushId && !pushP8Path) { return null; } // partial pushKey params were set, assume user has intention of passing it in if (!(pushId && pushP8Path && teamId)) { throw new Error('In order to provide a Push Key through the CLI parameters, you have to pass --push-id, --push-p8-path and --team-id parameters.'); } return { apnsKeyId: pushId, apnsKeyP8: await _fsExtra().default.readFile(pushP8Path, 'utf8'), teamId }; } async function usePushKeyFromParams(ctx, app, pushKey) { const isValid = await validatePushKey(ctx, pushKey); if (!isValid) { throw new Error('Cannot validate uploaded Push Key'); } const iosPushCredentials = await ctx.ios.createPushKey(app.accountName, pushKey); await ctx.ios.usePushKey(app, iosPushCredentials.id); _log().default.log(_chalk().default.green(`Successfully assigned Push Key to ${app.accountName}/${app.projectName} (${app.bundleIdentifier})`)); return iosPushCredentials; } //# sourceMappingURL=IosPushCredentials.js.map