diff --git a/.circleci/config.yml b/.circleci/config.yml index 8cce559..3304d81 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,8 +1,8 @@ version: 2.1 orbs: - cfa: continuousauth/npm@2.1.0 - node: electronjs/node@2.2.1 + cfa: continuousauth/npm@2.1.1 + node: electronjs/node@2.3.1 workflows: test_and_release: diff --git a/.github/workflows/add-to-project.yml b/.github/workflows/add-to-project.yml index ef10bed..d4a564e 100644 --- a/.github/workflows/add-to-project.yml +++ b/.github/workflows/add-to-project.yml @@ -21,7 +21,7 @@ jobs: creds: ${{ secrets.ECOSYSTEM_ISSUE_TRIAGE_GH_APP_CREDS }} org: electron - name: Add to Project - uses: dsanders11/project-actions/add-item@eb760c48894b5702398529cbb8f6e98378e315d0 # v1.3.0 + uses: dsanders11/project-actions/add-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0 with: field: Opened field-value: ${{ github.event.pull_request.created_at || github.event.issue.created_at }} diff --git a/.github/workflows/semantic.yml b/.github/workflows/semantic.yml index d4f3263..1b96a50 100644 --- a/.github/workflows/semantic.yml +++ b/.github/workflows/semantic.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: semantic-pull-request - uses: amannn/action-semantic-pull-request@cfb60706e18bc85e8aec535e3c577abe8f70378e # v5.5.2 + uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.gitignore b/.gitignore index 3dc0812..4cdb368 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules lib *.log src/example.ts +docs +.vscode diff --git a/.prettierrc.json b/.prettierrc.json index 632a3f5..e868094 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -3,5 +3,13 @@ "tabWidth": 2, "singleQuote": true, "printWidth": 100, - "parser": "typescript" + "parser": "typescript", + "overrides": [ + { + "files": ["*.json", "*.jsonc", "*.json5"], + "options": { + "parser": "json" + } + } + ] } \ No newline at end of file diff --git a/README.md b/README.md index c086055..3e69c1e 100644 --- a/README.md +++ b/README.md @@ -9,135 +9,181 @@ Electron Notarize ## Installation ```bash -# npm npm install @electron/notarize --save-dev - -# yarn -yarn add @electron/notarize --dev ``` ## What is app "notarization"? From Apple's docs in XCode: -> A notarized app is a macOS app that was uploaded to Apple for processing before it was distributed. When you export a notarized app from Xcode, it code signs the app with a Developer ID certificate and staples a ticket from Apple to the app. The ticket confirms that you previously uploaded the app to Apple. +> A notarized app is a macOS app that was uploaded to Apple for processing before it was distributed. +> When you export a notarized app from Xcode, it code signs the app with a Developer ID certificate +> and staples a ticket from Apple to the app. The ticket confirms that you previously uploaded the app to Apple. -> On macOS 10.14 and later, the user can launch notarized apps when Gatekeeper is enabled. When the user first launches a notarized app, Gatekeeper looks for the app’s ticket online. If the user is offline, Gatekeeper looks for the ticket that was stapled to the app. +> On macOS 10.14 and later, the user can launch notarized apps when Gatekeeper is enabled. +> When the user first launches a notarized app, Gatekeeper looks for the app’s ticket online. +> If the user is offline, Gatekeeper looks for the ticket that was stapled to the app. -Apple has made this a hard requirement as of 10.15 (Catalina). +As macOS 10.15 (Catalina), Apple has made notarization a hard requirement for all applications +distributed outside of the Mac App Store. App Store applications do not need to be notarized. ## Prerequisites For notarization, you need the following things: -1. Xcode 10 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: - 1. `com.apple.security.cs.allow-jit` +1. Xcode 13 or later installed on your Mac. +1. An [Apple Developer](https://developer.apple.com/) account. +1. [An app-specific password for your ADC account’s Apple ID](https://support.apple.com/HT204397). +1. Your app may need to be signed with `hardenedRuntime: true` option, with the `com.apple.security.cs.allow-jit` entitlement. + +> [!NOTE] +> If you are using Electron 11 or below, you must add the `com.apple.security.cs.allow-unsigned-executable-memory` entitlement too. +> When using version 12+, this entitlement should not be applied as it increases your app's attack surface. - If you are using Electron 11 or below, you must add the `com.apple.security.cs.allow-unsigned-executable-memory` entitlement too. - When using version 12+, this entitlement should not be applied as it increases your app's attack surface. ## API -### 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. - * `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 - * `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. - * ... 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. - * `appleApiIssuer` String - Your App Store Connect API key issuer, for example, `c055ca8c-e5a8-4836-b61d-aa5794eeb3f4`. Required if `appleApiKey` is specified. - * ... 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` - -1. Never hard code your password into your packaging scripts, use an environment -variable at a minimum. -2. It is possible to provide a keychain reference instead of your actual password (assuming that you have already logged into -the Application Loader from Xcode). For example: +`@electron/notarize` exposes a single `notarize` function that accepts the following parameters: +* `appPath` — the absolute path to your codesigned and packaged Electron application. +* `notarytoolPath` - String (optional) - Path to a custom notarytool binary ([more details](#custom-notarytool)) +* additional options required for authenticating your Apple ID (see below) -```javascript -const password = `@keychain:"Application Loader: ${appleId}"`; -``` +The method returns a void Promise once app notarization is complete. Please note that notarization may take +many minutes. -Another option is that you can add a new keychain item using either the Keychain Access app or from the command line using the `security` utility: +If the notarization process is unusually log for your application, see Apple Developer's docs to +[Avoid long notarization response times and size limits](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow#3561440). -```bash -security add-generic-password -a "AC_USERNAME" -w -s "AC_PASSWORD" -``` -where `AC_USERNAME` should be replaced with your Apple ID, and then in your code you can use: +### Usage with app-specific password + +You can generate an [app-specific password](https://support.apple.com/en-us/102654) for your Apple ID +to notarize your Electron applications. + +This method also requires you to specify the [Team ID](https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/) +of the Developer Team you want to notarize under. An Apple ID may be part of multiple Teams. ```javascript -const password = `@keychain:AC_PASSWORD`; +import { notarize } from '@electron/notarize'; + +await notarize({ + appPath, + appleId, // Login name of your Apple Developer account + appleIdPassword, // App-specific password + teamId, // Team ID for your developer team +}); ``` -## Notes on JWT authentication +> [!IMPORTANT] +> **Never hard code your app-specific password into your packaging scripts.** Use an environment +> variable at a minimum. -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`. +### Usage with App Store Connect API key -Based on the `ApiKey`, the legacy `altool` will look in the following places for that file: +Alternatively, you can also authenticate via JSON Web Token (JWT) with App Store Connect. -* `./private_keys` -* `~/private_keys` -* `~/.private_keys` -* `~/.appstoreconnect/private_keys` +You can obtain an API key from [App Store Connect](https://appstoreconnect.apple.com/access/integrations/api). +Create a **Team Key** (not an _Individual Key_) with **App Manager** access. -`notarytool` will not look for the key, and you must instead provide its path as the `appleApiKey` argument. +Note down the Issuer ID (UUID format) and Key ID (10-character alphanumeric string), +and download the `.p8` API key file (`AuthKey_.p8`). +For security purposes, the private key can only be downloaded once. -## Notes on your Team Short Name +Provide the absolute path to your API key as the `appleApiKey` argument. -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). +```javascript +import { notarize } from '@electron/notarize'; -With `Transporter` installed, run: -```sh -/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter -m provider -u APPLE_DEV_ACCOUNT -p APP_PASSWORD +await notarize({ + appPath, + appleApiKey, // Absolute path to API key (e.g. `/path/to/AuthKey_X0X0X0X0X0.p8`) + appleApiIssuer, // Issuer ID (e.g. `d5631714-a680-4b4b-8156-b4ed624c0845`) +}); ``` -Alternatively, with older versions of Xcode, run: +### Usage with Keychain credentials + +As an alternative to passing authentication options, you can also store your authentication +credentials (for both API key and app-specific password strategies) in the macOS Keychain +via the `xcrun notarytool` command-line utility. + +This method has the advantage of validating your notarization credentials before submitting +your application for notarization. + +For example: + ```sh -/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u APPLE_DEV_ACCOUNT -p APP_PASSWORD +# App-specific password strategy +xcrun notarytool store-credentials "my-app-password-profile" + --apple-id "" + --team-id + --password ``` -## Notes on your teamId +```sh +# App Store Connect API key strategy +xcrun notarytool store-credentials "my-api-key-profile" + --key "" + --key-id + --issuer +``` -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 +Successful storage of your credentials will look like this: -## Debug +``` +This process stores your credentials securely in the Keychain. You reference these credentials later using a profile name. -[`debug`](https://www.npmjs.com/package/debug) is used to display logs and messages. You can use `export DEBUG=electron-notarize*` to log additional debug information from this module. +Validating your credentials... +Success. Credentials validated. +Credentials saved to Keychain. +To use them, specify `--keychain-profile "my-api-key-profile"` +``` -## Example Usage +After successfully storing your credentials, pass the keychain profile name into +the `keychainProfile` parameter. ```javascript 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 - }); -} +await notarize({ + appPath, + keychainProfile, +}); +``` + +### Custom notarytool + +You can provide a path to a custom `notarytool`. This module allows this option to enable unique edge cases - but this use case is _explicitly unsupported_. + +## Troubleshooting + +### Debug logging + +[`debug`](https://www.npmjs.com/package/debug) is used to display logs and messages. +Run your notarization scripts with the `DEBUG=electron-notarize*` environment variable to log additional +debug information from this module. + +### Validating credentials + +When notarizing your application, you may run into issues with validating your notarization +credentials. + ``` +Error: HTTP status code: 401. Invalid credentials. Username or password is incorrect. +Use the app-specific password generated at appleid.apple.com. Ensure that all authentication arguments are correct. +``` + +[Storing your credentials in Keychain](#usage-with-keychain-credentials) will validate your credentials before +even GitHub. + +### Validating app notarization + +To validate that notarization worked, you can use the `stapler` command-line utility: + +```sh +stapler validate path/to/notarized.app +``` + +### Apple documentation + +Apple also provides additional debugging documentation on +[Resolving common notarization issues](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/resolving_common_notarization_issues). diff --git a/package.json b/package.json index b3031a6..ea8672c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "homepage": "https://github.com/electron/notarize#readme", "repository": { "type": "git", - "url": "https://github.com/electron/notarize.git" + "url": "git+https://github.com/electron/notarize.git" }, "bugs": { "url": "https://github.com/electron/notarize/issues" @@ -35,7 +35,9 @@ "jest": "^29.0.0", "prettier": "^1.18.2", "ts-jest": "^29.0.0", - "typescript": "^4.8.4" + "typedoc": "~0.25.13", + "typedoc-plugin-missing-exports": "^2.2.0", + "typescript": "4.9.3" }, "dependencies": { "debug": "^4.1.1", diff --git a/src/check-signature.ts b/src/check-signature.ts index ab77858..b5d38ba 100644 --- a/src/check-signature.ts +++ b/src/check-signature.ts @@ -1,21 +1,21 @@ import * as path from 'path'; import { spawn } from './spawn'; -import { NotarizeStapleOptions } from './types'; +import { NotaryToolNotarizeAppOptions } from './types'; import debug from 'debug'; const d = debug('electron-notarize'); -const codesignDisplay = async (opts: NotarizeStapleOptions) => { +const codesignDisplay = async (opts: NotaryToolNotarizeAppOptions) => { const result = await spawn('codesign', ['-dv', '-vvvv', '--deep', opts.appPath]); return result; }; -const codesign = async (opts: NotarizeStapleOptions) => { +const codesign = async (opts: NotaryToolNotarizeAppOptions) => { d('attempting to check codesign of app:', opts.appPath); const result = await spawn('codesign', ['-vvv', '--deep', '--strict', opts.appPath]); return result; }; -export async function checkSignatures(opts: NotarizeStapleOptions): Promise { +export async function checkSignatures(opts: NotaryToolNotarizeAppOptions): Promise { const fileExt = path.extname(opts.appPath); if (fileExt === '.dmg' || fileExt === '.pkg') { d('skipping codesign check for dmg or pkg file'); diff --git a/src/index.ts b/src/index.ts index 48fe71f..e47249e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,58 +2,62 @@ 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'; + +/** + * Sends your app to Apple for notarization with `notarytool` and staples a successful + * notarization result to the app bundle. This includes your {@link NotaryToolNotarizeAppOptions.appPath | appPath} + * as well as one of three valid credential authentication strategies. + * + * See {@link NotaryToolCredentials} for authentication options. + * + * @category Core + * @param args Options for notarization + * @returns The Promise resolves once notarization is complete. Note that this may take a few minutes. + */ +async function notarize(args: NotarizeOptionsNotaryTool): Promise; +/** + * @deprecated + */ +async function notarize(args: NotarizeOptionsLegacy): Promise; + +async function notarize({ appPath, ...otherOptions }: NotarizeOptions) { + if (otherOptions.tool === 'legacy') { + throw new Error( + 'Notarization with the legacy altool system was decommisioned as of November 2023', + ); + } -export async function notarize({ appPath, ...otherOptions }: NotarizeOptions) { await checkSignatures({ appPath }); - 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.', + d('notarizing using notarytool'); + if (!(await isNotaryToolAvailable())) { + throw new Error( + 'notarytool is not available, you must be on at least Xcode 13 or provide notarytoolPath', ); - 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.', - ); - 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 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/notarytool.ts b/src/notarytool.ts index 95381f4..0631ee1 100644 --- a/src/notarytool.ts +++ b/src/notarytool.ts @@ -12,6 +12,12 @@ import { NotaryToolCredentials, NotaryToolStartOptions } from './types'; const d = debug('electron-notarize:notarytool'); +function runNotaryTool(args: string[], notarytoolPath?: string) { + const useXcrun = notarytoolPath === undefined; + const cmd = useXcrun ? 'xcrun' : notarytoolPath; + return spawn(cmd, useXcrun ? ['notarytool', ...args] : args); +} + function authorizationArgs(rawOpts: NotaryToolCredentials): string[] { const opts = validateNotaryToolAuthorizationArgs(rawOpts); if (isNotaryToolPasswordCredentials(opts)) { @@ -41,9 +47,27 @@ function authorizationArgs(rawOpts: NotaryToolCredentials): string[] { } } -export async function isNotaryToolAvailable() { - const result = await spawn('xcrun', ['--find', 'notarytool']); - return result.code === 0; +async function getNotarizationLogs(opts: NotaryToolStartOptions, id: string) { + try { + const logResult = await runNotaryTool( + ['log', id, ...authorizationArgs(opts)], + opts.notarytoolPath, + ); + d('notarization log', logResult.output); + return logResult.output; + } catch (e) { + d('failed to pull notarization logs', e); + } +} + +export async function isNotaryToolAvailable(notarytoolPath?: string) { + if (notarytoolPath !== undefined) { + const result = await spawn(notarytoolPath, ['--version']); + return result.code === 0; + } else { + const result = await spawn('xcrun', ['--find', 'notarytool']); + return result.code === 0; + } } export async function notarizeAndWaitForNotaryTool(opts: NotaryToolStartOptions) { @@ -70,7 +94,6 @@ export async function notarizeAndWaitForNotaryTool(opts: NotaryToolStartOptions) } const notarizeArgs = [ - 'notarytool', 'submit', filePath, ...authorizationArgs(opts), @@ -79,25 +102,32 @@ export async function notarizeAndWaitForNotaryTool(opts: NotaryToolStartOptions) 'json', ]; - const result = await spawn('xcrun', notarizeArgs); - const parsed = JSON.parse(result.output.trim()); + const result = await runNotaryTool(notarizeArgs, opts.notarytoolPath); + const rawOut = result.output.trim(); - if (result.code !== 0 || !parsed.status || parsed.status !== 'Accepted') { - try { - if (parsed && parsed.id) { - const logResult = await spawn('xcrun', [ - 'notarytool', - 'log', - parsed.id, - ...authorizationArgs(opts), - ]); - d('notarization log', logResult.output); - } - } catch (e) { - d('failed to pull notarization logs', e); - } - throw new Error(`Failed to notarize via notarytool\n\n${result.output}`); + let parsed: any; + try { + parsed = JSON.parse(rawOut); + } catch (err) { + throw new Error( + `Failed to notarize via notarytool. Failed with unexpected result: \n\n${rawOut}`, + ); + } + + let logOutput: undefined | string; + if (typeof parsed.id === 'string') { + logOutput = await getNotarizationLogs(opts, parsed.id); + } + + if (result.code === 0 && parsed.status === 'Accepted') { + d(`notarization success (id: ${parsed.id})`); + return; + } + + let message = `Failed to notarize via notarytool\n\n${result.output}`; + if (logOutput) { + message += `\n\nDiagnostics from notarytool log: ${logOutput}`; } - d('notarization success'); + throw new Error(message); }); } diff --git a/src/staple.ts b/src/staple.ts index 67e45ec..92af881 100644 --- a/src/staple.ts +++ b/src/staple.ts @@ -1,11 +1,11 @@ import debug from 'debug'; import { spawn } from './spawn'; -import { NotarizeStapleOptions } from './types'; +import { NotaryToolNotarizeAppOptions } from './types'; const d = debug('electron-notarize:staple'); -export async function stapleApp(opts: NotarizeStapleOptions): Promise { +export async function stapleApp(opts: NotaryToolNotarizeAppOptions): Promise { d('attempting to staple app:', opts.appPath); const result = await spawn('xcrun', ['stapler', 'staple', '-v', opts.appPath]); diff --git a/src/types.ts b/src/types.ts index 78f7962..03a8bd6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,62 +1,187 @@ +/** + * @deprecated This interface was used for Apple's `altool`, which was sunset in 2023 and no longer works. + * @category Legacy + */ export interface LegacyNotarizePasswordCredentials { appleId: string; appleIdPassword: string; } +/** + * You can generate an [app-specific password](https://support.apple.com/en-us/102654) for your Apple ID + * to notarize your Electron applications. + * + * This method also requires you to specify the [Team ID](https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/) + * of the Developer Team you want to notarize under. An Apple ID may be part of multiple Teams. + * + * @category Credential Strategies + */ export interface NotaryToolPasswordCredentials { + /** + * The login username of your Apple Developer account. + */ appleId: string; + /** + * An [app-specific password](https://support.apple.com/en-us/102654) for your + * Apple ID (**not** your Apple ID password). + * + * Do **not** hard code this password into your packaging scripts. + */ appleIdPassword: string; + /** + * The [Team ID](https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/) + * for the Developer Team you want to notarize under. Your Apple ID may be a member of multiple + * teams. + */ teamId: string; } +/** + * @deprecated This interface was used for Apple's `altool`, which was sunset in 2023 and no longer works. + * @category Legacy + */ export interface LegacyNotarizeApiKeyCredentials { appleApiKey: string; appleApiIssuer: string; } +/** + * Credentials required for JSON Web Token (JWT) notarization using App Store Connect API keys. + * + * @category Credential Strategies + */ export interface NotaryToolApiKeyCredentials { + /** + * File system path to the `.p8` private key of your App Store Connect API key. + */ appleApiKey: string; + /** + * App Store Connect API Key ID (e.g. `T9GPZ92M7K`). + * + */ appleApiKeyId: string; + /** + * App Store Connect API Issuer ID. The issuer ID is a UUID format string + * (e.g. `c055ca8c-e5a8-4836-b61d-aa5794eeb3f4`). + */ appleApiIssuer: string; } +/** + * Options required for authenticating notarytool by storing + * credentials inside the system Keychain item. + * + * You can store {@link NotaryToolPasswordCredentials} or + * {@link NotaryToolApiKeyCredentials} into the Keychain + * using `xcrun notarytool store-credentials` and access the + * stored secrets when signing your code. + * + * @category Credential Strategies + */ export interface NotaryToolKeychainCredentials { + /** + * The name of the profile you provided when storing notarization credentials. + */ keychainProfile: string; + /** + * The name of the keychain or path to the keychain you stored notarization credentials in. + * @defaultValue If omitted, the system default `login` keychain will be used. + */ keychain?: string; } +/** + * @deprecated This interface was used for Apple's `altool`, which was sunset in 2023 and no longer works. + * @category Legacy + */ export type LegacyNotarizeCredentials = | LegacyNotarizePasswordCredentials | LegacyNotarizeApiKeyCredentials; + +/** + * Credential options for authenticating `notarytool`. There are three valid stategies available: + * + * - {@link NotaryToolPasswordCredentials} — Using an Apple ID and app-specific password + * - {@link NotaryToolApiKeyCredentials} — Using an App Store Connect API key + * - {@link NotaryToolKeychainCredentials} — Using one of the two above credential sets stored within the macOS Keychain + * @category Credential Strategies + */ export type NotaryToolCredentials = | NotaryToolPasswordCredentials | NotaryToolApiKeyCredentials | NotaryToolKeychainCredentials; -export type NotarizeCredentials = LegacyNotarizeCredentials | NotaryToolCredentials; +/** + * @deprecated This interface was used for Apple's `altool`, which was sunset in 2023 and no longer works. + * @category Legacy + */ export interface LegacyNotarizeAppOptions { appPath: string; appBundleId: string; } +/** + * Non-credential options for notarizing your application with `notarytool`. + * @category Core + */ export interface NotaryToolNotarizeAppOptions { + /** + * Absolute path to your packaged and codesigned Electron application. + */ appPath: string; + notarytoolPath?: string; } -export interface TransporterOptions { +/** + * @deprecated This interface was used for Apple's `altool`, which was sunset in 2023 and no longer works. + * @category Legacy + */ +interface TransporterOptions { ascProvider?: string; } -export interface NotarizeResult { +/** + * @deprecated This interface was used for Apple's `altool`, which was sunset in 2023 and no longer works. + * @category Legacy + */ +interface NotarizeResult { uuid: string; } +/** + * @deprecated This type was used for Apple's `altool`, which was sunset in 2023 and no longer works. + * @category Legacy + */ export type LegacyNotarizeStartOptions = LegacyNotarizeAppOptions & LegacyNotarizeCredentials & TransporterOptions; -export type NotaryToolStartOptions = NotaryToolNotarizeAppOptions & NotaryToolCredentials; + +/** + * @deprecated This type was used for Apple's `altool`, which was sunset in 2023 and no longer works. + * @category Legacy + */ export type LegacyNotarizeWaitOptions = NotarizeResult & LegacyNotarizeCredentials; -export type NotarizeStapleOptions = Pick; -export type NotarizeOptions = - | ({ tool?: 'legacy' } & LegacyNotarizeStartOptions) - | ({ tool: 'notarytool' } & NotaryToolStartOptions); + +/** + * @deprecated This type was used for Apple's `altool`, which was sunset in 2023 and no longer works. + * @category Legacy + */ +export type NotarizeOptionsLegacy = { tool: 'legacy' } & LegacyNotarizeStartOptions; + +/** + * Options for notarizing your Electron app with `notarytool`. + * @category Core + */ +export type NotaryToolStartOptions = NotaryToolNotarizeAppOptions & NotaryToolCredentials; + +/** + * Helper type that specifies that `@electron/notarize` is using the `notarytool` strategy. + * @category Utility Types + */ +export type NotarizeOptionsNotaryTool = { tool?: 'notarytool' } & NotaryToolStartOptions; + +/** + * Options accepted by the `notarize` method. + * @internal + */ +export type NotarizeOptions = NotarizeOptionsLegacy | NotarizeOptionsNotaryTool; diff --git a/src/validate-args.ts b/src/validate-args.ts index 0b3be5c..3e56372 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 { @@ -86,6 +89,9 @@ export function isNotaryToolKeychainCredentials( return creds.keychain !== undefined || creds.keychainProfile !== undefined; } +/** + * @internal + */ export function validateNotaryToolAuthorizationArgs( opts: NotaryToolCredentials, ): NotaryToolCredentials { diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..fddfeea --- /dev/null +++ b/typedoc.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["./src/index.ts"], + "plugin": ["typedoc-plugin-missing-exports"], + "placeInternalsInOwningModule": true, + "excludeInternal": true, + "navigation": { + "includeCategories": true + }, + "defaultCategory": "Utility", + "categoryOrder": [ + "Core", + "Credential Strategies", + "Utility Types", + "Legacy" + ] +} diff --git a/yarn.lock b/yarn.lock index 8e22993..8d7acb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -805,6 +805,11 @@ ansi-regex@^5.0.0, ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-sequence-parser@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz#e0aa1cdcbc8f8bb0b5bca625aac41f5f056973cf" + integrity sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -925,12 +930,19 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browserslist@^4.21.3: version "4.21.4" @@ -1209,10 +1221,10 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -1817,6 +1829,11 @@ json5@^2.2.1: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonc-parser@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" + integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== + jsonfile@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" @@ -1860,6 +1877,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lunr@^2.3.9: + version "2.3.9" + resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" + integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== + make-dir@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -1879,17 +1901,22 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +marked@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" + integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== micromatch@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" mimic-fn@^2.1.0: @@ -1904,6 +1931,13 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@^9.0.3: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -2123,6 +2157,16 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shiki@^0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.7.tgz#c3c9e1853e9737845f1d2ef81b31bcfb07056d4e" + integrity sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg== + dependencies: + ansi-sequence-parser "^1.1.0" + jsonc-parser "^3.2.0" + vscode-oniguruma "^1.7.0" + vscode-textmate "^8.0.0" + signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -2294,7 +2338,22 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -typescript@^4.8.4: +typedoc-plugin-missing-exports@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-2.2.0.tgz#e39a04bab5b0d7f7b28507d64c07a4c40b788648" + integrity sha512-2+XR1IcyQ5UwXZVJe9NE6HrLmNufT9i5OwoIuuj79VxuA3eYq+Y6itS9rnNV1D7UeQnUSH8kISYD73gHE5zw+w== + +typedoc@~0.25.13: + version "0.25.13" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.13.tgz#9a98819e3b2d155a6d78589b46fa4c03768f0922" + integrity sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ== + dependencies: + lunr "^2.3.9" + marked "^4.3.0" + minimatch "^9.0.3" + shiki "^0.14.7" + +typescript@4.9.3: version "4.9.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db" integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== @@ -2321,6 +2380,16 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" +vscode-oniguruma@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" + integrity sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA== + +vscode-textmate@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-8.0.0.tgz#2c7a3b1163ef0441097e0b5d6389cd5504b59e5d" + integrity sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg== + walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f"