diff --git a/README.md b/README.md index c086055..aa62268 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Apple has made this a hard requirement as of 10.15 (Catalina). For notarization, you need the following things: -1. Xcode 10 or later installed on your Mac. +1. Xcode 13 or later installed on your Mac. 2. An [Apple Developer](https://developer.apple.com/) account. 3. [An app-specific password for your ADC account’s Apple ID](https://support.apple.com/HT204397). 4. Your app may need to be signed with `hardened-runtime`, including the following entitlement: @@ -44,13 +44,13 @@ For notarization, you need the following things: ### Method: `notarize(opts): Promise` * `options` Object - * `tool` String - The notarization tool to use, default is `notarytool`. Can be `legacy` or `notarytool`. `notarytool` is substantially (10x) faster and `legacy` is deprecated and will **stop working** on November 1st 2023. + * `tool` String - The notarization tool to use, default is `notarytool`. Previously, the value `legacy` used `altool`, which [**stopped working** on November 1st 2023](https://developer.apple.com/news/?id=y5mjxqmn). * `appPath` String - The absolute path to your `.app` file - * There are different options for each tool: Notarytool - * There are three authentication methods available: user name with password: - * `appleId` String - The username of your apple developer account + * There are three authentication methods available: + * user name with password: + * `appleId` String - The username of your Apple Developer account * `appleIdPassword` String - The [app-specific password](https://support.apple.com/HT204397) (not your Apple ID password). - * `teamId` String - The team ID you want to notarize under. + * `teamId` String - The [team ID](https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/) you want to notarize under. * ... or apiKey with apiIssuer: * `appleApiKey` String - Absolute path to the `.p8` file containing the key. Required for JWT authentication. See Note on JWT authentication below. * `appleApiKeyId` String - App Store Connect API key ID, for example, `T9GPZ92M7K`. Required for JWT authentication. See Note on JWT authentication below. @@ -58,15 +58,6 @@ For notarization, you need the following things: * ... or keychain with keychainProfile: * `keychain` String (optional) - The name of the keychain or path to the keychain you stored notarization credentials in. If omitted, iCloud keychain is used by default. * `keychainProfile` String - The name of the profile you provided when storing notarization credentials. - * ... or Legacy - * `appBundleId` String - The app bundle identifier your Electron app is using. E.g. `com.github.electron` - * `ascProvider` String (optional) - Your [Team Short Name](#notes-on-your-team-short-name). - * There are two authentication methods available: user name with password: - * `appleId` String - The username of your apple developer account - * `appleIdPassword` String - The [app-specific password](https://support.apple.com/HT204397) (not your Apple ID password). - * ... or apiKey with apiIssuer: - * `appleApiKey` String - Required for JWT authentication. See Note on JWT authentication below. - * `appleApiIssuer` String - Issuer ID. Required if `appleApiKey` is specified. ## Safety when using `appleIdPassword` @@ -92,34 +83,11 @@ const password = `@keychain:AC_PASSWORD`; ## Notes on JWT authentication -You can obtain an API key from [Appstore Connect](https://appstoreconnect.apple.com/access/api). Create a key with _App Manager_ access. Note down the Issuer ID and download the `.p8` file. This file is your API key and comes with the name of `AuthKey_.p8`. This is the string you have to supply when calling `notarize`. - -Based on the `ApiKey`, the legacy `altool` will look in the following places for that file: - -* `./private_keys` -* `~/private_keys` -* `~/.private_keys` -* `~/.appstoreconnect/private_keys` - -`notarytool` will not look for the key, and you must instead provide its path as the `appleApiKey` argument. - -## Notes on your Team Short Name - -If you are a member of multiple teams or organizations, you have to tell Apple on behalf of which organization you're uploading. To find your [team's short name](https://forums.developer.apple.com/thread/113798)), you can ask `iTMSTransporter`, which is part of the now deprecated `Application Loader` as well as the newer [`Transporter`](https://apps.apple.com/us/app/transporter/id1450874784?mt=12). - -With `Transporter` installed, run: -```sh -/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter -m provider -u APPLE_DEV_ACCOUNT -p APP_PASSWORD -``` - -Alternatively, with older versions of Xcode, run: -```sh -/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u APPLE_DEV_ACCOUNT -p APP_PASSWORD -``` +You can obtain an API key from [App Store Connect](https://appstoreconnect.apple.com/access/api). Create a _Team Key_ (not an _Individual Key_) with _App Manager_ access. Note down the Issuer ID and download the `.p8` file. This file is your API key and comes with the name of `AuthKey_.p8`. Provide the path to this file as the `appleApiKey` argument. ## Notes on your teamId -If you use the new Notary Tool method with `appleId`/`appleIdPassword` you will need to set the `teamId` option. To get this ID, go to your [Apple Developer Account](https://developer.apple.com/account), then click on "Membership details", and there you will find your Team ID. This link should get you there directly: https://developer.apple.com/account#MembershipDetailsCard +To get your `teamId` value, go to your [Apple Developer Account](https://developer.apple.com/account), then click on "Membership details", and there you will find your Team ID. ## Debug @@ -133,11 +101,10 @@ import { notarize } from '@electron/notarize'; async function packageTask () { // Package your app here, and code sign with hardened runtime await notarize({ - appBundleId, appPath, appleId, appleIdPassword, - ascProvider, // This parameter is optional + teamId, }); } ``` diff --git a/src/index.ts b/src/index.ts index 48fe71f..ac45c13 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,58 +2,47 @@ import debug from 'debug'; import retry from 'promise-retry'; import { checkSignatures } from './check-signature'; -import { delay } from './helpers'; -import { startLegacyNotarize, waitForLegacyNotarize } from './legacy'; import { isNotaryToolAvailable, notarizeAndWaitForNotaryTool } from './notarytool'; import { stapleApp } from './staple'; -import { NotarizeOptions, NotaryToolStartOptions } from './types'; +import { + NotarizeOptions, + NotaryToolStartOptions, + NotarizeOptionsLegacy, + NotarizeOptionsNotaryTool, +} from './types'; const d = debug('electron-notarize'); export { NotarizeOptions }; -export { validateLegacyAuthorizationArgs as validateAuthorizationArgs } from './validate-args'; +export { validateNotaryToolAuthorizationArgs as validateAuthorizationArgs } from './validate-args'; -export async function notarize({ appPath, ...otherOptions }: NotarizeOptions) { - await checkSignatures({ appPath }); +async function notarize(args: NotarizeOptionsNotaryTool): Promise; +/** @deprecated */ +async function notarize(args: NotarizeOptionsLegacy): Promise; +async function notarize({ appPath, ...otherOptions }: NotarizeOptions) { if (otherOptions.tool === 'legacy') { - console.warn( - 'Notarizing using the legacy altool system. The altool system will be disabled on November 1 2023. Please switch to the notarytool system before then.', - ); - console.warn( - 'You can do this by setting "tool: notarytool" in your "@electron/notarize" options. Please note that the credentials options may be slightly different between tools.', + throw new Error( + 'Notarization with the legacy altool system was decommisioned as of November 2023', ); - d('notarizing using the legacy notarization system, this will be slow'); - const { uuid } = await startLegacyNotarize({ - appPath, - ...otherOptions, - }); - /** - * Wait for Apples API to initialize the status UUID - * - * If we start checking too quickly the UUID is not ready yet - * and this step will fail. It takes Apple a number of minutes - * to actually complete the job so an extra 10 second delay here - * is not a big deal - */ - d('notarization started, waiting for 10 seconds before pinging Apple for status'); - await delay(10000); - d('starting to poll for notarization status'); - await waitForLegacyNotarize({ uuid, ...otherOptions }); - } else { - d('notarizing using the new notarytool system'); - if (!(await isNotaryToolAvailable())) { - throw new Error('notarytool is not available, you must be on at least Xcode 13'); - } - - await notarizeAndWaitForNotaryTool({ - appPath, - ...otherOptions, - } as NotaryToolStartOptions); } + await checkSignatures({ appPath }); + + d('notarizing using notarytool'); + if (!(await isNotaryToolAvailable())) { + throw new Error('notarytool is not available, you must be on at least Xcode 13'); + } + + await notarizeAndWaitForNotaryTool({ + appPath, + ...otherOptions, + } as NotaryToolStartOptions); + await retry(() => stapleApp({ appPath }), { retries: 3, }); } + +export { notarize }; diff --git a/src/legacy.ts b/src/legacy.ts index c9ce044..c0fb056 100644 --- a/src/legacy.ts +++ b/src/legacy.ts @@ -1,129 +1,18 @@ import debug from 'debug'; -import * as path from 'path'; -import { spawn } from './spawn'; -import { withTempDir, makeSecret, parseNotarizationInfo, delay } from './helpers'; -import { validateLegacyAuthorizationArgs, isLegacyPasswordCredentials } from './validate-args'; -import { - NotarizeResult, - LegacyNotarizeStartOptions, - LegacyNotarizeWaitOptions, - LegacyNotarizeCredentials, -} from './types'; +import { LegacyNotarizeStartOptions, LegacyNotarizeWaitOptions } from './types'; const d = debug('electron-notarize:legacy'); -function authorizationArgs(rawOpts: LegacyNotarizeCredentials): string[] { - const opts = validateLegacyAuthorizationArgs(rawOpts); - if (isLegacyPasswordCredentials(opts)) { - return ['-u', makeSecret(opts.appleId), '-p', makeSecret(opts.appleIdPassword)]; - } else { - return [ - '--apiKey', - makeSecret(opts.appleApiKey), - '--apiIssuer', - makeSecret(opts.appleApiIssuer), - ]; - } -} - -export async function startLegacyNotarize( - opts: LegacyNotarizeStartOptions, -): Promise { +/** @deprecated */ +export async function startLegacyNotarize(opts: LegacyNotarizeStartOptions): Promise { d('starting notarize process for app:', opts.appPath); - return await withTempDir(async dir => { - const zipPath = path.resolve(dir, `${path.basename(opts.appPath, '.app')}.zip`); - d('zipping application to:', zipPath); - const zipResult = await spawn( - 'ditto', - ['-c', '-k', '--sequesterRsrc', '--keepParent', path.basename(opts.appPath), zipPath], - { - cwd: path.dirname(opts.appPath), - }, - ); - if (zipResult.code !== 0) { - throw new Error( - `Failed to zip application, exited with code: ${zipResult.code}\n\n${zipResult.output}`, - ); - } - d('zip succeeded, attempting to upload to Apple'); - - const notarizeArgs = [ - 'altool', - '--notarize-app', - '-f', - zipPath, - '--primary-bundle-id', - opts.appBundleId, - ...authorizationArgs(opts), - ]; - - if (opts.ascProvider) { - notarizeArgs.push('-itc_provider', opts.ascProvider); - } - - const result = await spawn('xcrun', notarizeArgs); - if (result.code !== 0) { - throw new Error(`Failed to upload app to Apple's notarization servers\n\n${result.output}`); - } - d('upload success'); - - const uuidMatch = /\nRequestUUID = (.+?)\n/g.exec(result.output); - if (!uuidMatch) { - throw new Error(`Failed to find request UUID in output:\n\n${result.output}`); - } - - d('found UUID:', uuidMatch[1]); - - return { - uuid: uuidMatch[1], - }; - }); + throw new Error('Cannot start notarization. Legacy notarization (altool) is no longer available'); } -export async function waitForLegacyNotarize(opts: LegacyNotarizeWaitOptions): Promise { - d('checking notarization status:', opts.uuid); - const result = await spawn('xcrun', [ - 'altool', - '--notarization-info', - opts.uuid, - ...authorizationArgs(opts), - ]); - if (result.code !== 0) { - // These checks could fail for all sorts of reasons, including: - // * The status of a request isn't available as soon as the upload request returns, so - // it may just not be ready yet. - // * If using keychain password, user's mac went to sleep and keychain locked. - // * Regular old connectivity failure. - d( - `Failed to check status of notarization request, retrying in 30 seconds: ${opts.uuid}\n\n${result.output}`, - ); - await delay(30000); - return waitForLegacyNotarize(opts); - } - const notarizationInfo = parseNotarizationInfo(result.output); - - if (notarizationInfo.status === 'in progress') { - d('still in progress, waiting 30 seconds'); - await delay(30000); - return waitForLegacyNotarize(opts); - } - - d('notarzation done with info:', notarizationInfo); - - if (notarizationInfo.status === 'invalid') { - d('notarization failed'); - throw new Error(`Apple failed to notarize your application, check the logs for more info - -Status Code: ${notarizationInfo.statusCode || 'No Code'} -Message: ${notarizationInfo.statusMessage || 'No Message'} -Logs: ${notarizationInfo.logFileUrl}`); - } - - if (notarizationInfo.status !== 'success') { - throw new Error(`Unrecognized notarization status: "${notarizationInfo.status}"`); - } - - d('notarization was successful'); - return; +/** @deprecated */ +export async function waitForLegacyNotarize(opts: LegacyNotarizeWaitOptions): Promise { + throw new Error( + 'Cannot wait for notarization. Legacy notarization (altool) is no longer available', + ); } diff --git a/src/types.ts b/src/types.ts index 78f7962..a360cba 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +/** @deprecated */ export interface LegacyNotarizePasswordCredentials { appleId: string; appleIdPassword: string; @@ -9,6 +10,7 @@ export interface NotaryToolPasswordCredentials { teamId: string; } +/** @deprecated */ export interface LegacyNotarizeApiKeyCredentials { appleApiKey: string; appleApiIssuer: string; @@ -25,6 +27,7 @@ export interface NotaryToolKeychainCredentials { keychain?: string; } +/** @deprecated */ export type LegacyNotarizeCredentials = | LegacyNotarizePasswordCredentials | LegacyNotarizeApiKeyCredentials; @@ -34,6 +37,7 @@ export type NotaryToolCredentials = | NotaryToolKeychainCredentials; export type NotarizeCredentials = LegacyNotarizeCredentials | NotaryToolCredentials; +/** @deprecated */ export interface LegacyNotarizeAppOptions { appPath: string; appBundleId: string; @@ -51,12 +55,16 @@ export interface NotarizeResult { uuid: string; } +/** @deprecated */ export type LegacyNotarizeStartOptions = LegacyNotarizeAppOptions & LegacyNotarizeCredentials & TransporterOptions; export type NotaryToolStartOptions = NotaryToolNotarizeAppOptions & NotaryToolCredentials; +/** @deprecated */ export type LegacyNotarizeWaitOptions = NotarizeResult & LegacyNotarizeCredentials; export type NotarizeStapleOptions = Pick; -export type NotarizeOptions = - | ({ tool?: 'legacy' } & LegacyNotarizeStartOptions) - | ({ tool: 'notarytool' } & NotaryToolStartOptions); + +/** @deprecated */ +export type NotarizeOptionsLegacy = { tool: 'legacy' } & LegacyNotarizeStartOptions; +export type NotarizeOptionsNotaryTool = { tool?: 'notarytool' } & NotaryToolStartOptions; +export type NotarizeOptions = NotarizeOptionsLegacy | NotarizeOptionsNotaryTool; diff --git a/src/validate-args.ts b/src/validate-args.ts index 0b3be5c..8ab7d52 100644 --- a/src/validate-args.ts +++ b/src/validate-args.ts @@ -8,6 +8,7 @@ import { NotaryToolPasswordCredentials, } from './types'; +/** @deprecated */ export function isLegacyPasswordCredentials( opts: LegacyNotarizeCredentials, ): opts is LegacyNotarizePasswordCredentials { @@ -15,6 +16,7 @@ export function isLegacyPasswordCredentials( return creds.appleId !== undefined || creds.appleIdPassword !== undefined; } +/** @deprecated */ export function isLegacyApiKeyCredentials( opts: LegacyNotarizeCredentials, ): opts is LegacyNotarizeApiKeyCredentials { @@ -22,6 +24,7 @@ export function isLegacyApiKeyCredentials( return creds.appleApiKey !== undefined || creds.appleApiIssuer !== undefined; } +/** @deprecated */ export function validateLegacyAuthorizationArgs( opts: LegacyNotarizeCredentials, ): LegacyNotarizeCredentials {