diff --git a/.changeset/app-dev-non-interactive-flags.md b/.changeset/app-dev-non-interactive-flags.md new file mode 100644 index 00000000000..f59065d305c --- /dev/null +++ b/.changeset/app-dev-non-interactive-flags.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Add non-interactive `app dev` options: `--store-password` and `--install-mkcert`. diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index d2157241446..0ec90ceec51 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -979,6 +979,15 @@ "isOptional": true, "environmentValue": "SHOPIFY_FLAG_CLIENT_ID" }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-dev.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--install-mkcert", + "value": "''", + "description": "Install and use mkcert to generate localhost certificates when --use-localhost is enabled without prompting.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_INSTALL_MKCERT" + }, { "filePath": "docs-shopify.dev/commands/interfaces/app-dev.interface.ts", "syntaxKind": "PropertySignature", @@ -1042,6 +1051,15 @@ "isOptional": true, "environmentValue": "SHOPIFY_FLAG_SKIP_DEPENDENCIES_INSTALLATION" }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-dev.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--store-password ", + "value": "string", + "description": "The password for storefronts with password protection.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_STORE_PASSWORD" + }, { "filePath": "docs-shopify.dev/commands/interfaces/app-dev.interface.ts", "syntaxKind": "PropertySignature", @@ -1115,7 +1133,7 @@ "environmentValue": "SHOPIFY_FLAG_THEME" } ], - "value": "export interface appdev {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias '?: string\n\n /**\n * Resource URL for checkout UI extension. Format: \"/cart/{productVariantID}:{productQuantity}\"\n * @environment SHOPIFY_FLAG_CHECKOUT_CART_URL\n */\n '--checkout-cart-url '?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Port to use for localhost. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_LOCALHOST_PORT\n */\n '--localhost-port '?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Uses the app URL from the toml file instead an autogenerated URL for dev.\n * @environment SHOPIFY_FLAG_NO_UPDATE\n */\n '--no-update'?: ''\n\n /**\n * The file path or URL. The file path is to a file that you want updated on idle. The URL path is where you want a webhook posted to report on file changes.\n * @environment SHOPIFY_FLAG_NOTIFY\n */\n '--notify '?: string\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Skips the installation of dependencies. Deprecated, use workspaces instead.\n * @environment SHOPIFY_FLAG_SKIP_DEPENDENCIES_INSTALLATION\n */\n '--skip-dependencies-installation'?: ''\n\n /**\n * Store URL. Must be an existing development or Shopify Plus sandbox store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store '?: string\n\n /**\n * Resource URL for subscription UI extension. Format: \"/products/{productId}\"\n * @environment SHOPIFY_FLAG_SUBSCRIPTION_PRODUCT_URL\n */\n '--subscription-product-url '?: string\n\n /**\n * Theme ID or name of the theme app extension host theme.\n * @environment SHOPIFY_FLAG_THEME\n */\n '-t, --theme '?: string\n\n /**\n * Local port of the theme app extension development server. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_THEME_APP_EXTENSION_PORT\n */\n '--theme-app-extension-port '?: string\n\n /**\n * Use a custom tunnel, it must be running before executing dev. Format: \"https://my-tunnel-url:port\".\n * @environment SHOPIFY_FLAG_TUNNEL_URL\n */\n '--tunnel-url '?: string\n\n /**\n * Service entry point will listen to localhost. A tunnel won't be used. Will work for testing many app features, but not those that directly invoke your app (E.g: Webhooks)\n * @environment SHOPIFY_FLAG_USE_LOCALHOST\n */\n '--use-localhost'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" + "value": "export interface appdev {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias '?: string\n\n /**\n * Resource URL for checkout UI extension. Format: \"/cart/{productVariantID}:{productQuantity}\"\n * @environment SHOPIFY_FLAG_CHECKOUT_CART_URL\n */\n '--checkout-cart-url '?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Install and use mkcert to generate localhost certificates when --use-localhost is enabled without prompting.\n * @environment SHOPIFY_FLAG_INSTALL_MKCERT\n */\n '--install-mkcert'?: ''\n\n /**\n * Port to use for localhost. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_LOCALHOST_PORT\n */\n '--localhost-port '?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Uses the app URL from the toml file instead an autogenerated URL for dev.\n * @environment SHOPIFY_FLAG_NO_UPDATE\n */\n '--no-update'?: ''\n\n /**\n * The file path or URL. The file path is to a file that you want updated on idle. The URL path is where you want a webhook posted to report on file changes.\n * @environment SHOPIFY_FLAG_NOTIFY\n */\n '--notify '?: string\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Skips the installation of dependencies. Deprecated, use workspaces instead.\n * @environment SHOPIFY_FLAG_SKIP_DEPENDENCIES_INSTALLATION\n */\n '--skip-dependencies-installation'?: ''\n\n /**\n * Store URL. Must be an existing development or Shopify Plus sandbox store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store '?: string\n\n /**\n * The password for storefronts with password protection.\n * @environment SHOPIFY_FLAG_STORE_PASSWORD\n */\n '--store-password '?: string\n\n /**\n * Resource URL for subscription UI extension. Format: \"/products/{productId}\"\n * @environment SHOPIFY_FLAG_SUBSCRIPTION_PRODUCT_URL\n */\n '--subscription-product-url '?: string\n\n /**\n * Theme ID or name of the theme app extension host theme.\n * @environment SHOPIFY_FLAG_THEME\n */\n '-t, --theme '?: string\n\n /**\n * Local port of the theme app extension development server. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_THEME_APP_EXTENSION_PORT\n */\n '--theme-app-extension-port '?: string\n\n /**\n * Use a custom tunnel, it must be running before executing dev. Format: \"https://my-tunnel-url:port\".\n * @environment SHOPIFY_FLAG_TUNNEL_URL\n */\n '--tunnel-url '?: string\n\n /**\n * Service entry point will listen to localhost. A tunnel won't be used. Will work for testing many app features, but not those that directly invoke your app (E.g: Webhooks)\n * @environment SHOPIFY_FLAG_USE_LOCALHOST\n */\n '--use-localhost'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" } }, "appenvpull": { diff --git a/packages/app/src/cli/api/graphql/convert_dev_to_transfer_disabled_store.ts b/packages/app/src/cli/api/graphql/convert_dev_to_transfer_disabled_store.ts deleted file mode 100644 index 51976dccb1f..00000000000 --- a/packages/app/src/cli/api/graphql/convert_dev_to_transfer_disabled_store.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {gql} from 'graphql-request' - -export const ConvertDevToTransferDisabledStoreQuery = gql` - mutation convertDevToTestStore($input: ConvertDevToTestStoreInput!) { - convertDevToTestStore(input: $input) { - convertedToTestStore - userErrors { - message - field - } - } - } -` - -export interface ConvertDevToTransferDisabledStoreVariables { - input: { - organizationID: number - shopId: string - } -} - -export interface ConvertDevToTransferDisabledSchema { - convertDevToTestStore: { - convertedToTestStore: boolean - userErrors: { - field: string[] - message: string - }[] - } -} diff --git a/packages/app/src/cli/commands/app/dev.test.ts b/packages/app/src/cli/commands/app/dev.test.ts new file mode 100644 index 00000000000..26ac10c63ee --- /dev/null +++ b/packages/app/src/cli/commands/app/dev.test.ts @@ -0,0 +1,72 @@ +import Dev from './dev.js' +import {dev} from '../../services/dev.js' +import {linkedAppContext} from '../../services/app-context.js' +import {storeContext} from '../../services/store-context.js' +import {getTunnelMode} from '../../services/dev/tunnel-mode.js' +import {checkFolderIsValidApp} from '../../models/app/loader.js' +import { + testAppLinked, + testDeveloperPlatformClient, + testOrganization, + testOrganizationApp, + testOrganizationStore, + testProject, +} from '../../models/app/app.test-data.js' +import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs' +import {addPublicMetadata} from '@shopify/cli-kit/node/metadata' +import {beforeEach, describe, expect, test, vi} from 'vitest' + +vi.mock('../../services/dev.js') +vi.mock('../../services/app-context.js') +vi.mock('../../services/store-context.js') +vi.mock('../../services/dev/tunnel-mode.js') +vi.mock('../../models/app/loader.js') +vi.mock('@shopify/cli-kit/node/metadata') + +describe('app dev command', () => { + beforeEach(() => { + vi.mocked(dev).mockReset() + vi.mocked(linkedAppContext).mockReset() + vi.mocked(storeContext).mockReset() + vi.mocked(getTunnelMode).mockReset() + vi.mocked(checkFolderIsValidApp).mockReset() + vi.mocked(addPublicMetadata).mockReset() + }) + + test('does not require --use-localhost when --install-mkcert is not passed', async () => { + await inTemporaryDirectory(async (tmp) => { + const app = testAppLinked({directory: tmp}) + const appContextResult = { + app, + remoteApp: testOrganizationApp(), + organization: testOrganization(), + project: testProject(), + activeConfig: {} as never, + specifications: [], + developerPlatformClient: testDeveloperPlatformClient(), + } as Awaited> + const store = testOrganizationStore({shopDomain: 'dev-store.myshopify.com'}) + + vi.mocked(linkedAppContext).mockResolvedValue(appContextResult) + vi.mocked(storeContext).mockResolvedValue(store) + vi.mocked(getTunnelMode).mockResolvedValue({mode: 'auto'}) + + await Dev.run(['--path', tmp, '--store', store.shopDomain], import.meta.url) + + expect(getTunnelMode).toHaveBeenCalledWith({ + useLocalhost: false, + tunnelUrl: undefined, + localhostPort: undefined, + }) + expect(dev).toHaveBeenCalledWith(expect.objectContaining({installMkcert: false, tunnel: {mode: 'auto'}})) + }) + }) + + test('requires --use-localhost when --install-mkcert is passed', async () => { + await inTemporaryDirectory(async (tmp) => { + await expect(Dev.run(['--path', tmp, '--install-mkcert'], import.meta.url)).rejects.toThrow() + + expect(dev).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/app/src/cli/commands/app/dev.ts b/packages/app/src/cli/commands/app/dev.ts index 17edc962a05..ee6f217ee97 100644 --- a/packages/app/src/cli/commands/app/dev.ts +++ b/packages/app/src/cli/commands/app/dev.ts @@ -54,9 +54,14 @@ export default class Dev extends AppLinkedCommand { description: "Service entry point will listen to localhost. A tunnel won't be used. Will work for testing many app features, but not those that directly invoke your app (E.g: Webhooks)", env: 'SHOPIFY_FLAG_USE_LOCALHOST', - default: false, exclusive: ['tunnel-url'], }), + 'install-mkcert': Flags.boolean({ + description: + 'Install and use mkcert to generate localhost certificates when --use-localhost is enabled without prompting.', + env: 'SHOPIFY_FLAG_INSTALL_MKCERT', + dependsOn: ['use-localhost'], + }), 'localhost-port': portFlag({ description: 'Port to use for localhost.', env: 'SHOPIFY_FLAG_LOCALHOST_PORT', @@ -70,6 +75,10 @@ export default class Dev extends AppLinkedCommand { description: 'Local port of the theme app extension development server.', env: 'SHOPIFY_FLAG_THEME_APP_EXTENSION_PORT', }), + 'store-password': Flags.string({ + description: 'The password for storefronts with password protection.', + env: 'SHOPIFY_FLAG_STORE_PASSWORD', + }), notify: Flags.string({ description: 'The file path or URL. The file path is to a file that you want updated on idle. The URL path is where you want a webhook posted to report on file changes.', @@ -96,7 +105,7 @@ export default class Dev extends AppLinkedCommand { const {flags} = await this.parse(Dev) const tunnelMode = await getTunnelMode({ - useLocalhost: flags['use-localhost'], + useLocalhost: flags['use-localhost'] ?? false, tunnelUrl: flags['tunnel-url'], localhostPort: flags['localhost-port'], }) @@ -134,9 +143,11 @@ export default class Dev extends AppLinkedCommand { checkoutCartUrl: flags['checkout-cart-url'], theme: flags.theme, themeExtensionPort: flags['theme-app-extension-port'], + storePassword: flags['store-password'], notify: flags.notify, graphiqlPort: flags['graphiql-port'], graphiqlKey: flags['graphiql-key'], + installMkcert: flags['install-mkcert'] ?? false, tunnel: tunnelMode, } diff --git a/packages/app/src/cli/models/app/app.test-data.ts b/packages/app/src/cli/models/app/app.test-data.ts index b60f0b612c4..53d2e66f0fd 100644 --- a/packages/app/src/cli/models/app/app.test-data.ts +++ b/packages/app/src/cli/models/app/app.test-data.ts @@ -41,7 +41,6 @@ import { } from '../../utilities/developer-platform-client.js' import {AllAppExtensionRegistrationsQuerySchema} from '../../api/graphql/all_app_extension_registrations.js' import {AppDeploySchema, AppDeployVariables} from '../../api/graphql/app_deploy.js' -import {ConvertDevToTransferDisabledStoreVariables} from '../../api/graphql/convert_dev_to_transfer_disabled_store.js' import {SendSampleWebhookSchema, SendSampleWebhookVariables} from '../../services/webhook/request-sample.js' import {PublicApiVersionsSchema} from '../../services/webhook/request-api-versions.js' import {WebhookTopicsSchema, WebhookTopicsVariables} from '../../services/webhook/request-topics.js' @@ -1241,13 +1240,6 @@ const generateSignedUploadUrlResponse: AssetUrlSchema = { userErrors: [], } -const convertedToTransferDisabledStoreResponse = { - convertDevToTestStore: { - convertedToTestStore: true, - userErrors: [], - }, -} - const organizationsResponse: Organization[] = [testOrganization()] const sendSampleWebhookResponse: SendSampleWebhookSchema = { @@ -1339,8 +1331,6 @@ export function testDeveloperPlatformClient(stubs: Partial Promise.resolve(deployResponse), release: (_input: {app: MinimalAppIdentifiers; version: AppVersionIdentifiers}) => Promise.resolve(releaseResponse), generateSignedUploadUrl: (_app: MinimalAppIdentifiers) => Promise.resolve(generateSignedUploadUrlResponse), - convertToTransferDisabledStore: (_input: ConvertDevToTransferDisabledStoreVariables) => - Promise.resolve(convertedToTransferDisabledStoreResponse), sendSampleWebhook: (_input: SendSampleWebhookVariables) => Promise.resolve(sendSampleWebhookResponse), apiVersions: () => Promise.resolve(apiVersionsResponse), topics: (_input: WebhookTopicsVariables) => Promise.resolve(topicsResponse), diff --git a/packages/app/src/cli/prompts/dev.ts b/packages/app/src/cli/prompts/dev.ts index e8c249fda76..651b5ff7c24 100644 --- a/packages/app/src/cli/prompts/dev.ts +++ b/packages/app/src/cli/prompts/dev.ts @@ -111,15 +111,6 @@ export async function selectStorePrompt({ return currentStores.find((store) => store.shopId === id) } -export async function confirmConversionToTransferDisabledStorePrompt(): Promise { - return renderConfirmationPrompt({ - message: `Make this store transfer-disabled? For security, once you use a dev store to preview an app locally, the store can never be transferred to a merchant to use as a production store.`, - confirmationMessage: 'Yes, make this store transfer-disabled permanently', - cancellationMessage: 'No, select another store', - defaultValue: false, - }) -} - export async function appNamePrompt(currentName: string): Promise { return renderTextPrompt({ message: 'App name', diff --git a/packages/app/src/cli/services/dev.ts b/packages/app/src/cli/services/dev.ts index 0dfcb3b09e1..24e0b135bb0 100644 --- a/packages/app/src/cli/services/dev.ts +++ b/packages/app/src/cli/services/dev.ts @@ -57,9 +57,11 @@ export interface DevOptions { tunnel: TunnelMode theme?: string themeExtensionPort?: number + storePassword?: string notify?: string graphiqlPort?: number graphiqlKey?: string + installMkcert?: boolean } export async function dev(commandOptions: DevOptions) { @@ -141,6 +143,7 @@ async function prepareForDev(commandOptions: DevOptions): Promise { tunnel, tunnelClient, remoteApp.configuration, + commandOptions.installMkcert, ) app.webs = webs @@ -250,6 +253,7 @@ async function setupNetworkingOptions( tunnelOptions: TunnelMode, tunnelClient?: TunnelClient, remoteAppConfig?: AppConfigurationUsedByCli, + installMkcert?: boolean, ) { const {backendConfig, frontendConfig} = frontAndBackendConfig(webs) @@ -287,6 +291,7 @@ async function setupNetworkingOptions( if (tunnelOptions.mode === 'use-localhost') { const {keyContent, certContent, certPath} = await generateCertificate({ appDirectory, + forceInstall: installMkcert, }) reverseProxyCert = { diff --git a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts index f0ec1c92b1c..1277bb675d0 100644 --- a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts +++ b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts @@ -169,6 +169,7 @@ export async function setupDevProcesses({ storeFqdn, theme: commandOptions.theme, themeExtensionPort: commandOptions.themeExtensionPort, + storePassword: commandOptions.storePassword, }), setupSendUninstallWebhookProcess({ webs: reloadedApp.webs, diff --git a/packages/app/src/cli/services/dev/processes/theme-app-extension.test.ts b/packages/app/src/cli/services/dev/processes/theme-app-extension.test.ts index 870405d9f4d..166a1322b0a 100644 --- a/packages/app/src/cli/services/dev/processes/theme-app-extension.test.ts +++ b/packages/app/src/cli/services/dev/processes/theme-app-extension.test.ts @@ -212,6 +212,24 @@ describe('setupPreviewThemeAppExtensionsProcess', () => { expect(result!.options.storefrontPassword).toEqual(storefrontPassword) expect(result!.options.crawlerSignatureHeaders).toEqual(crawlerSignatureHeaders) }) + + test('uses the provided store password when one is required', async () => { + const mockTheme = {id: 123} as Theme + vi.mocked(fetchTheme).mockResolvedValue(mockTheme) + vi.mocked(isStorefrontPasswordProtected).mockResolvedValue(true) + vi.mocked(ensureValidPassword).mockResolvedValue('validated-password') + + const result = await setupPreviewThemeAppExtensionsProcess({ + localApp: testApp({allExtensions: [await testThemeExtensions()]}), + remoteApp: testOrganizationApp(), + storeFqdn: 'test.myshopify.com', + theme: '1', + storePassword: 'provided-password', + }) + + expect(ensureValidPassword).toHaveBeenCalledWith('provided-password', 'test.myshopify.com', crawlerSignatureHeaders) + expect(result!.options.storefrontPassword).toEqual('validated-password') + }) }) describe('findOrCreateHostTheme', () => { diff --git a/packages/app/src/cli/services/dev/processes/theme-app-extension.ts b/packages/app/src/cli/services/dev/processes/theme-app-extension.ts index c9147f3505b..d4065b78d90 100644 --- a/packages/app/src/cli/services/dev/processes/theme-app-extension.ts +++ b/packages/app/src/cli/services/dev/processes/theme-app-extension.ts @@ -33,6 +33,7 @@ interface HostThemeSetupOptions { storeFqdn: string theme?: string themeExtensionPort?: number + storePassword?: string } export interface PreviewThemeAppExtensionsProcess extends BaseProcess { @@ -64,7 +65,7 @@ export async function setupPreviewThemeAppExtensionsProcess( isStorefrontPasswordProtected(adminSession), ]) const storefrontPassword = isPasswordProtected - ? await ensureValidPassword(undefined, storeFqdn, crawlerSignatureHeaders) + ? await ensureValidPassword(options.storePassword, storeFqdn, crawlerSignatureHeaders) : undefined const theme = await findOrCreateHostTheme(adminSession, options.theme) diff --git a/packages/app/src/cli/services/dev/select-store.test.ts b/packages/app/src/cli/services/dev/select-store.test.ts index 75f88a12268..68a1ad446da 100644 --- a/packages/app/src/cli/services/dev/select-store.test.ts +++ b/packages/app/src/cli/services/dev/select-store.test.ts @@ -1,10 +1,6 @@ import {selectStore} from './select-store.js' import {Organization, OrganizationSource, OrganizationStore} from '../../models/organization.js' -import { - reloadStoreListPrompt, - selectStorePrompt, - confirmConversionToTransferDisabledStorePrompt, -} from '../../prompts/dev.js' +import {reloadStoreListPrompt, selectStorePrompt} from '../../prompts/dev.js' import {testDeveloperPlatformClient} from '../../models/app/app.test-data.js' import {ClientName} from '../../utilities/developer-platform-client.js' import {sleep} from '@shopify/cli-kit/node/system' @@ -93,51 +89,25 @@ describe('selectStore', async () => { ) }) - test('prompts user to convert store to non-transferable if selection is invalid', async () => { + test('throws if selected store is not transfer-disabled', async () => { // Given vi.mocked(selectStorePrompt).mockResolvedValueOnce(STORE2) - vi.mocked(confirmConversionToTransferDisabledStorePrompt).mockResolvedValueOnce(true) // When - const got = await selectStore( - {stores: [STORE1, STORE2], hasMorePages: false}, - ORG1, - testDeveloperPlatformClient({clientName: ClientName.Partners}), - ) - - // Then - expect(got).toEqual(STORE2) - expect(selectStorePrompt).toHaveBeenCalledWith( - expect.objectContaining({ - stores: [STORE1, STORE2], - showDomainOnPrompt: defaultShowDomainOnPrompt, - }), - ) - expect(confirmConversionToTransferDisabledStorePrompt).toHaveBeenCalled() - }) - - test('choosing not to convert to transfer-disabled forces another prompt', async () => { - // Given - vi.mocked(selectStorePrompt).mockResolvedValueOnce(STORE2) - vi.mocked(selectStorePrompt).mockResolvedValueOnce(STORE1) - vi.mocked(confirmConversionToTransferDisabledStorePrompt).mockResolvedValueOnce(false) - - // When - const got = await selectStore( + const got = selectStore( {stores: [STORE1, STORE2], hasMorePages: false}, ORG1, testDeveloperPlatformClient({clientName: ClientName.Partners}), ) // Then - expect(got).toEqual(STORE1) + await expect(got).rejects.toThrow('The store you specified (domain2) is not transfer-disabled') expect(selectStorePrompt).toHaveBeenCalledWith( expect.objectContaining({ stores: [STORE1, STORE2], showDomainOnPrompt: defaultShowDomainOnPrompt, }), ) - expect(confirmConversionToTransferDisabledStorePrompt).toHaveBeenCalled() }) test('throws if store is non convertible', async () => { diff --git a/packages/app/src/cli/services/dev/select-store.ts b/packages/app/src/cli/services/dev/select-store.ts index 000ef277971..f201d696d53 100644 --- a/packages/app/src/cli/services/dev/select-store.ts +++ b/packages/app/src/cli/services/dev/select-store.ts @@ -1,18 +1,9 @@ import {Organization, OrganizationStore} from '../../models/organization.js' -import { - confirmConversionToTransferDisabledStorePrompt, - reloadStoreListPrompt, - selectStorePrompt, -} from '../../prompts/dev.js' -import { - ConvertDevToTransferDisabledSchema, - ConvertDevToTransferDisabledStoreVariables, -} from '../../api/graphql/convert_dev_to_transfer_disabled_store.js' +import {reloadStoreListPrompt, selectStorePrompt} from '../../prompts/dev.js' import {ClientName, DeveloperPlatformClient, Paginateable} from '../../utilities/developer-platform-client.js' import {sleep} from '@shopify/cli-kit/node/system' import {renderInfo, renderTasks} from '@shopify/cli-kit/node/ui' -import {AbortError, BugError, CancelExecution} from '@shopify/cli-kit/node/error' -import {outputSuccess} from '@shopify/cli-kit/node/output' +import {AbortError, CancelExecution} from '@shopify/cli-kit/node/error' /** * Select store from list or @@ -31,8 +22,8 @@ export async function selectStore( ): Promise { const showDomainOnPrompt = developerPlatformClient.clientName === ClientName.AppManagement const onSearchForStoresByName = async (term: string) => developerPlatformClient.devStoresForOrg(org.id, term) - // If no stores, guide the developer through creating one - // Then, with a store selected, make sure its transfer-disabled, prompting to convert if needed + // If no stores, guide the developer through creating one. + // Then, with a store selected, make sure it's transfer-disabled. let store = await selectStorePrompt({ onSearchForStoresByName, ...storesSearch, @@ -53,21 +44,7 @@ export async function selectStore( store = await selectStore({stores, hasMorePages: false}, org, developerPlatformClient) } - let storeIsValid = await convertToTransferDisabledStoreIfNeeded( - store, - org.id, - developerPlatformClient, - 'prompt-first', - ) - while (!storeIsValid) { - // eslint-disable-next-line no-await-in-loop - store = await selectStorePrompt({stores: [store], hasMorePages: false, showDomainOnPrompt}) - if (!store) { - throw new CancelExecution() - } - // eslint-disable-next-line no-await-in-loop - storeIsValid = await convertToTransferDisabledStoreIfNeeded(store, org.id, developerPlatformClient, 'prompt-first') - } + ensureTransferDisabledStore(store) return store } @@ -110,23 +87,13 @@ async function waitForCreatedStore( /** * Check if the store exists in the current organization and it is a valid store - * To be valid, it must be non-transferable. This can't be undone, so we ask the user to confirm. + * To be valid, it must be transfer-disabled. * - * @param storeDomain - Store domain to check - * @param stores - List of available stores - * @param orgId - Current organization ID - * @param developerPlatformClient - The client to access the platform API - * @param conversionMode - Whether to prompt the user to convert the store to a transfer-disabled store, or fail if it's not - * @returns False, if the store is invalid and the user chose not to convert it. Otherwise true. - * @throws If the store can't be found in the organization or we fail to make it a transfer-disabled store + * @param store - Store to check + * @throws If the store is not eligible for `app dev` */ -export async function convertToTransferDisabledStoreIfNeeded( - store: OrganizationStore, - orgId: string, - developerPlatformClient: DeveloperPlatformClient, - conversionMode: 'prompt-first' | 'never', -): Promise { - if (store.transferDisabled) return true +export function ensureTransferDisabledStore(store: OrganizationStore): void { + if (store.transferDisabled) return if (!store.transferDisabled && !store.convertableToPartnerTest) { throw new AbortError( @@ -135,50 +102,8 @@ export async function convertToTransferDisabledStoreIfNeeded( ) } - switch (conversionMode) { - case 'prompt-first': { - const confirmed = await confirmConversionToTransferDisabledStorePrompt() - if (!confirmed) { - // tell caller the store is invalid and not converted. they may re-prompt etc. - return false - } - await convertStoreToTransferDisabled(store, orgId, developerPlatformClient) - return true - } - case 'never': { - throw new AbortError( - 'The store you specified is not transfer-disabled', - "Try running 'dev --reset' and selecting a different store, or choosing to convert this one.", - ) - } - } -} - -/** - * Convert a store to a transfer-disabled store so development apps can be installed - * @param store - Store to convert - * @param orgId - Current organization ID - * @param developerPlatformClient - The client to access the platform API - */ -async function convertStoreToTransferDisabled( - store: OrganizationStore, - orgId: string, - developerPlatformClient: DeveloperPlatformClient, -) { - const variables: ConvertDevToTransferDisabledStoreVariables = { - input: { - organizationID: parseInt(orgId, 10), - shopId: store.shopId, - }, - } - const result: ConvertDevToTransferDisabledSchema = - await developerPlatformClient.convertToTransferDisabledStore(variables) - if (!result.convertDevToTestStore.convertedToTestStore) { - const errors = result.convertDevToTestStore.userErrors.map((error) => error.message).join(', ') - throw new BugError( - `Error converting store ${store.shopDomain} to a transfer-disabled store: ${errors}`, - 'This store might not be compatible with draft apps, please try a different store', - ) - } - outputSuccess(`Converted ${store.shopDomain} to a transfer-disabled store`) + throw new AbortError( + `The store you specified (${store.shopDomain}) is not transfer-disabled`, + 'Run dev --reset and select a transfer-disabled dev store.', + ) } diff --git a/packages/app/src/cli/services/store-context.test.ts b/packages/app/src/cli/services/store-context.test.ts index bb83f8538da..7312aeb52c1 100644 --- a/packages/app/src/cli/services/store-context.test.ts +++ b/packages/app/src/cli/services/store-context.test.ts @@ -1,6 +1,6 @@ import {storeContext} from './store-context.js' import {fetchStore} from './dev/fetch.js' -import {convertToTransferDisabledStoreIfNeeded, selectStore} from './dev/select-store.js' +import {ensureTransferDisabledStore, selectStore} from './dev/select-store.js' import {LoadedAppContextOutput} from './app-context.js' import { testAppLinked, @@ -66,12 +66,7 @@ describe('storeContext', () => { mockDeveloperPlatformClient, ['APP_DEVELOPMENT'], ) - expect(convertToTransferDisabledStoreIfNeeded).toHaveBeenCalledWith( - mockStore, - mockOrganization.id, - mockDeveloperPlatformClient, - 'never', - ) + expect(ensureTransferDisabledStore).toHaveBeenCalledWith(mockStore) expect(result).toEqual(mockStore) }) }) diff --git a/packages/app/src/cli/services/store-context.ts b/packages/app/src/cli/services/store-context.ts index 42827d58bb1..05f9f9b3673 100644 --- a/packages/app/src/cli/services/store-context.ts +++ b/packages/app/src/cli/services/store-context.ts @@ -1,5 +1,5 @@ import {fetchStore} from './dev/fetch.js' -import {convertToTransferDisabledStoreIfNeeded, selectStore} from './dev/select-store.js' +import {ensureTransferDisabledStore, selectStore} from './dev/select-store.js' import {LoadedAppContextOutput} from './app-context.js' import {OrganizationStore} from '../models/organization.js' import {Store} from '../utilities/developer-platform-client.js' @@ -54,9 +54,8 @@ export async function storeContext({ if (storeFqdnToUse) { selectedStore = await fetchStore(organization, storeFqdnToUse, developerPlatformClient, storeTypes) - // never automatically convert a store provided via the command line if (isDevStoresOnly) { - await convertToTransferDisabledStoreIfNeeded(selectedStore, organization.id, developerPlatformClient, 'never') + ensureTransferDisabledStore(selectedStore) } } else { // If no storeFqdn is provided, fetch all stores for the organization and let the user select one. diff --git a/packages/app/src/cli/utilities/developer-platform-client.ts b/packages/app/src/cli/utilities/developer-platform-client.ts index f092e6d1d1b..dac97fd382a 100644 --- a/packages/app/src/cli/utilities/developer-platform-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client.ts @@ -11,10 +11,6 @@ import { import {AllAppExtensionRegistrationsQuerySchema} from '../api/graphql/all_app_extension_registrations.js' import {AppDeploySchema, AppDeployVariables} from '../api/graphql/app_deploy.js' -import { - ConvertDevToTransferDisabledSchema, - ConvertDevToTransferDisabledStoreVariables, -} from '../api/graphql/convert_dev_to_transfer_disabled_store.js' import {AppVersionsQuerySchema} from '../api/graphql/get_versions_list.js' import {AppReleaseSchema} from '../api/graphql/app_release.js' import {AppVersionsDiffSchema} from '../api/graphql/app_versions_diff.js' @@ -257,9 +253,6 @@ export interface DeveloperPlatformClient { generateSignedUploadUrl: (app: MinimalAppIdentifiers) => Promise deploy: (input: AppDeployOptions) => Promise release: (input: {app: MinimalOrganizationApp; version: AppVersionIdentifiers}) => Promise - convertToTransferDisabledStore: ( - input: ConvertDevToTransferDisabledStoreVariables, - ) => Promise sendSampleWebhook: (input: SendSampleWebhookVariables, organizationId: string) => Promise apiVersions: (organizationId: string) => Promise topics: (input: WebhookTopicsVariables, organizationId: string) => Promise diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts index ff9fae67966..a6e1d30c822 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts @@ -44,10 +44,6 @@ import { } from '../../api/graphql/all_app_extension_registrations.js' import {AppDeploySchema} from '../../api/graphql/app_deploy.js' import {AppVersionsQuerySchema as AppVersionsQuerySchemaInterface} from '../../api/graphql/get_versions_list.js' -import { - ConvertDevToTransferDisabledSchema, - ConvertDevToTransferDisabledStoreVariables, -} from '../../api/graphql/convert_dev_to_transfer_disabled_store.js' import {AppReleaseSchema} from '../../api/graphql/app_release.js' import {AppVersionsDiffSchema} from '../../api/graphql/app_versions_diff.js' import { @@ -897,12 +893,6 @@ export class AppManagementClient implements DeveloperPlatformClient { } } - async convertToTransferDisabledStore( - _input: ConvertDevToTransferDisabledStoreVariables, - ): Promise { - throw new BugError('Not implemented: convertToTransferDisabledStore') - } - async sendSampleWebhook(input: SendSampleWebhookVariables, organizationId: string): Promise { const query = CliTesting const variables = { diff --git a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts index fa2dafdc36e..8e31833517b 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts @@ -39,11 +39,6 @@ import { GenerateSignedUploadUrlSchema, GenerateSignedUploadUrlVariables, } from '../../api/graphql/generate_signed_upload_url.js' -import { - ConvertDevToTransferDisabledStoreQuery, - ConvertDevToTransferDisabledSchema, - ConvertDevToTransferDisabledStoreVariables, -} from '../../api/graphql/convert_dev_to_transfer_disabled_store.js' import { FindStoreByDomainQuery, FindStoreByDomainQueryVariables, @@ -470,12 +465,6 @@ export class PartnersClient implements DeveloperPlatformClient { } } - async convertToTransferDisabledStore( - input: ConvertDevToTransferDisabledStoreVariables, - ): Promise { - return this.request(ConvertDevToTransferDisabledStoreQuery, input) - } - async storeByDomain(orgId: string, shopDomain: string, _storeTypes: Store[]): Promise { // Note: storeTypes filtering not implemented for PartnersClient const variables: FindStoreByDomainQueryVariables = {orgId, shopDomain} diff --git a/packages/app/src/cli/utilities/mkcert.test.ts b/packages/app/src/cli/utilities/mkcert.test.ts index 02b396cb396..36940aa85fa 100644 --- a/packages/app/src/cli/utilities/mkcert.test.ts +++ b/packages/app/src/cli/utilities/mkcert.test.ts @@ -216,6 +216,44 @@ describe('mkcert', () => { expect(downloadGitHubRelease).not.toHaveBeenCalled() }) + testWithTempDir('generates a certificate without prompting when forceInstall is true', async ({tempDir}) => { + const appDirectory = tempDir + vi.mocked(generateCertificatePrompt).mockClear() + vi.mocked(exec).mockClear() + + vi.mocked(exec).mockImplementation(async () => { + await mkdir(joinPath(appDirectory, '.shopify')) + await writeFile(joinPath(appDirectory, '.shopify', 'localhost-key.pem'), 'key') + await writeFile(joinPath(appDirectory, '.shopify', 'localhost.pem'), 'cert') + }) + + await generateCertificate({ + appDirectory, + forceInstall: true, + platform: 'linux', + }) + + expect(generateCertificatePrompt).not.toHaveBeenCalled() + expect(exec).toHaveBeenCalled() + }) + + testWithTempDir( + 'fails without prompting when forceInstall is false and certificates are missing', + async ({tempDir}) => { + vi.mocked(generateCertificatePrompt).mockClear() + vi.mocked(exec).mockClear() + const generatePromise = generateCertificate({ + appDirectory: tempDir, + forceInstall: false, + platform: 'linux', + }) + + await expect(generatePromise).rejects.toThrow(AbortError) + expect(generateCertificatePrompt).not.toHaveBeenCalled() + expect(exec).not.toHaveBeenCalled() + }, + ) + testWithTempDir('skips certificate generation if the certificate already exists', async ({tempDir}) => { const appDirectory = tempDir await mkdir(joinPath(appDirectory, '.shopify')) diff --git a/packages/app/src/cli/utilities/mkcert.ts b/packages/app/src/cli/utilities/mkcert.ts index b5f97f70dcd..f0ea73f3bd0 100644 --- a/packages/app/src/cli/utilities/mkcert.ts +++ b/packages/app/src/cli/utilities/mkcert.ts @@ -121,6 +121,7 @@ async function downloadMkcertLicense(dotShopifyPath: string): Promise { const currentDate = new Date(Date.UTC(2022, 1, 1, 10, 0, 0)) let publishEventMock: MockedFunction @@ -202,7 +210,7 @@ describe('event tracking', () => { await inProjectWithFile('package.json', async (args) => { // Given const commandContent = {command: 'dev', topic: 'app'} - const argsWithPassword = args.concat(['--password', 'shptka_abc123']) + const argsWithPassword = args.concat(['--password', 'shptka_abc123', '--store-password', 'store-secret']) await startAnalytics({commandContent, args: argsWithPassword, currentTime: currentDate.getTime() - 100}) // When @@ -214,7 +222,7 @@ describe('event tracking', () => { // Then const expectedPayloadSensitive = { - args: expect.stringMatching(/.*password \*\*\*\*\*/), + args: expect.stringMatching(/.*password \*\*\*\*\*.*store-password \*\*\*\*\*/), metadata: expect.anything(), } expect(publishEventMock).toHaveBeenCalledOnce() @@ -222,36 +230,69 @@ describe('event tracking', () => { }) }) - test('sends SHOPIFY_ environment variables in sensitive payload', async () => { - const originalEnv = {...process.env} - process.env.SHOPIFY_TEST_VAR = 'test_value' - process.env.SHOPIFY_ANOTHER_VAR = 'another_value' - process.env.NOT_SHOPIFY_VAR = 'should_not_appear' - + test('does not send store password environment flags to Monorail', async () => { await inProjectWithFile('package.json', async (args) => { const commandContent = {command: 'dev', topic: 'app'} await startAnalytics({commandContent, args, currentTime: currentDate.getTime() - 100}) + await addSensitiveMetadata(() => ({ + environmentFlags: JSON.stringify({'store-password': 'store-secret'}), + })) - // When const config = { runHook: vi.fn().mockResolvedValue({successes: [], failures: []}), plugins: [], } as any await reportAnalyticsEvent({config, exitMode: 'ok'}) - // Then - const sensitivePayload = publishEventMock.mock.calls[0]![2] expect(publishEventMock).toHaveBeenCalledOnce() - expect(sensitivePayload).toHaveProperty('env_shopify_variables') - expect(sensitivePayload.env_shopify_variables).toBeDefined() - - const shopifyVars = JSON.parse(sensitivePayload.env_shopify_variables as string) - expect(shopifyVars).toHaveProperty('SHOPIFY_TEST_VAR', 'test_value') - expect(shopifyVars).toHaveProperty('SHOPIFY_ANOTHER_VAR', 'another_value') - expect(shopifyVars).not.toHaveProperty('NOT_SHOPIFY_VAR') + expect(publishEventMock.mock.calls[0]![2]).toMatchObject({ + cmd_all_environment_flags: JSON.stringify({'store-password': '*****'}), + }) }) }) + test('sends SHOPIFY_ environment variables in sensitive payload', async () => { + const originalShopifyTestVar = process.env.SHOPIFY_TEST_VAR + const originalShopifyAnotherVar = process.env.SHOPIFY_ANOTHER_VAR + const originalShopifyFlagStorePassword = process.env.SHOPIFY_FLAG_STORE_PASSWORD + const originalNotShopifyVar = process.env.NOT_SHOPIFY_VAR + process.env.SHOPIFY_TEST_VAR = 'test_value' + process.env.SHOPIFY_ANOTHER_VAR = 'another_value' + process.env.SHOPIFY_FLAG_STORE_PASSWORD = 'store-secret' + process.env.NOT_SHOPIFY_VAR = 'should_not_appear' + + try { + await inProjectWithFile('package.json', async (args) => { + const commandContent = {command: 'dev', topic: 'app'} + await startAnalytics({commandContent, args, currentTime: currentDate.getTime() - 100}) + + // When + const config = { + runHook: vi.fn().mockResolvedValue({successes: [], failures: []}), + plugins: [], + } as any + await reportAnalyticsEvent({config, exitMode: 'ok'}) + + // Then + const sensitivePayload = publishEventMock.mock.calls[0]![2] + expect(publishEventMock).toHaveBeenCalledOnce() + expect(sensitivePayload).toHaveProperty('env_shopify_variables') + expect(sensitivePayload.env_shopify_variables).toBeDefined() + + const shopifyVars = JSON.parse(sensitivePayload.env_shopify_variables as string) + expect(shopifyVars).toHaveProperty('SHOPIFY_TEST_VAR', 'test_value') + expect(shopifyVars).toHaveProperty('SHOPIFY_ANOTHER_VAR', 'another_value') + expect(shopifyVars).toHaveProperty('SHOPIFY_FLAG_STORE_PASSWORD', '*****') + expect(shopifyVars).not.toHaveProperty('NOT_SHOPIFY_VAR') + }) + } finally { + restoreEnvVariable('SHOPIFY_TEST_VAR', originalShopifyTestVar) + restoreEnvVariable('SHOPIFY_ANOTHER_VAR', originalShopifyAnotherVar) + restoreEnvVariable('SHOPIFY_FLAG_STORE_PASSWORD', originalShopifyFlagStorePassword) + restoreEnvVariable('NOT_SHOPIFY_VAR', originalNotShopifyVar) + } + }) + test('does nothing when analytics are disabled', async () => { await inProjectWithFile('package.json', async (args) => { // Given diff --git a/packages/cli-kit/src/public/node/analytics.ts b/packages/cli-kit/src/public/node/analytics.ts index 3ee7f2c466d..dcaef13d51c 100644 --- a/packages/cli-kit/src/public/node/analytics.ts +++ b/packages/cli-kit/src/public/node/analytics.ts @@ -198,7 +198,10 @@ async function buildPayload({config, errorMessage, exitMode}: ReportAnalyticsEve function sanitizePayload(payload: T): T { const payloadString = JSON.stringify(payload) // Remove Theme Access passwords from the payload - const sanitizedPayloadString = payloadString.replace(/shptka_\w*/g, '*****') + const sanitizedPayloadString = payloadString + .replace(/shptka_\w*/g, '*****') + .replace(/(--store-password(?:=|\s+))(?:"[^"]*"|'[^']*'|[^\s"]+)/g, '$1*****') + .replace(/((?:store-password|SHOPIFY_FLAG_STORE_PASSWORD)\\?":\\?")[^"\\]*/g, '$1*****') return JSON.parse(sanitizedPayloadString) } diff --git a/packages/cli/README.md b/packages/cli/README.md index 60be6630998..764f6563de0 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -653,9 +653,10 @@ Run the app. ``` USAGE $ shopify app dev [--auth-alias ] [--checkout-cart-url ] [--client-id | -c ] - [--localhost-port ] [--no-color] [--no-update] [--notify ] [--path ] [--reset | ] - [--skip-dependencies-installation] [-s ] [--subscription-product-url ] [-t ] - [--theme-app-extension-port ] [--use-localhost | [--tunnel-url | ]] [--verbose] + [--install-mkcert --use-localhost] [--localhost-port ] [--no-color] [--no-update] [--notify ] [--path + ] [--reset | ] [--skip-dependencies-installation] [-s ] [--store-password ] + [--subscription-product-url ] [-t ] [--theme-app-extension-port ] [--tunnel-url | ] + [--verbose] FLAGS -c, --config= @@ -682,6 +683,10 @@ FLAGS The Client ID of your app. [env: SHOPIFY_FLAG_CLIENT_ID] + --install-mkcert + Install and use mkcert to generate localhost certificates when --use-localhost is enabled without prompting. + [env: SHOPIFY_FLAG_INSTALL_MKCERT] + --localhost-port= Port to use for localhost. Must be between 1 and 65535. [env: SHOPIFY_FLAG_LOCALHOST_PORT] @@ -711,6 +716,10 @@ FLAGS Skips the installation of dependencies. Deprecated, use workspaces instead. [env: SHOPIFY_FLAG_SKIP_DEPENDENCIES_INSTALLATION] + --store-password= + The password for storefronts with password protection. + [env: SHOPIFY_FLAG_STORE_PASSWORD] + --subscription-product-url= Resource URL for subscription UI extension. Format: "/products/{productId}" [env: SHOPIFY_FLAG_SUBSCRIPTION_PRODUCT_URL] diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 98fcb227fe2..2c18645f3af 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -1057,6 +1057,16 @@ "name": "graphiql-port", "type": "option" }, + "install-mkcert": { + "allowNo": false, + "dependsOn": [ + "use-localhost" + ], + "description": "Install and use mkcert to generate localhost certificates when --use-localhost is enabled without prompting.", + "env": "SHOPIFY_FLAG_INSTALL_MKCERT", + "name": "install-mkcert", + "type": "boolean" + }, "localhost-port": { "description": "Port to use for localhost. Must be between 1 and 65535.", "env": "SHOPIFY_FLAG_LOCALHOST_PORT", @@ -1124,6 +1134,14 @@ "name": "store", "type": "option" }, + "store-password": { + "description": "The password for storefronts with password protection.", + "env": "SHOPIFY_FLAG_STORE_PASSWORD", + "hasDynamicHelp": false, + "multiple": false, + "name": "store-password", + "type": "option" + }, "subscription-product-url": { "description": "Resource URL for subscription UI extension. Format: \"/products/{productId}\"", "env": "SHOPIFY_FLAG_SUBSCRIPTION_PRODUCT_URL",