From 5064d438c018b406d6b629df3d1343b758668fb2 Mon Sep 17 00:00:00 2001 From: Donald Merand Date: Tue, 29 Sep 2026 18:08:25 -0400 Subject: [PATCH 1/4] Use client IDs for app management and dev requests --- .../app-dev/generated/dev-session-create.ts | 8 +- .../app-dev/generated/dev-session-delete.ts | 8 +- .../app-dev/generated/dev-session-update.ts | 8 +- .../queries/dev-session-create.graphql | 4 +- .../queries/dev-session-delete.graphql | 4 +- .../queries/dev-session-update.graphql | 4 +- .../generated/active-app-release.ts | 171 ----------- .../generated/app-install-count.ts | 13 +- .../generated/app-version-by-tag.ts | 11 + .../app-management/generated/app-versions.ts | 13 +- .../generated/create-app-version.ts | 10 +- .../generated/create-source-scan.ts | 10 +- .../generated/release-version.ts | 10 +- .../request-source-scan-upload-url.ts | 10 +- .../queries/active-app-release.graphql | 6 - .../queries/app-install-count.graphql | 4 +- .../queries/app-version-by-tag.graphql | 4 +- .../queries/app-versions.graphql | 4 +- .../queries/create-app-version.graphql | 4 +- .../queries/create-source-scan.graphql | 4 +- .../queries/release-version.graphql | 4 +- .../request-source-scan-upload-url.graphql | 4 +- .../app/security/submit.integration.test.ts | 9 +- .../services/app-security-submit-api.test.ts | 7 +- .../cli/services/app-security-submit-api.ts | 4 +- .../app/src/cli/services/dev-clean.test.ts | 6 +- packages/app/src/cli/services/dev-clean.ts | 2 +- packages/app/src/cli/services/dev.ts | 2 +- .../dev-session/dev-session-process.test.ts | 19 +- .../dev/processes/dev-session/dev-session.ts | 4 +- packages/app/src/cli/services/dev/ui.test.tsx | 10 +- packages/app/src/cli/services/dev/ui.tsx | 4 +- .../utilities/developer-platform-client.ts | 6 +- .../app-management-client.test.ts | 280 ++++++++++++++++-- .../app-management-client.ts | 57 ++-- 35 files changed, 415 insertions(+), 313 deletions(-) diff --git a/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-create.ts b/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-create.ts index 44996257f84..72be5e23515 100644 --- a/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-create.ts +++ b/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-create.ts @@ -5,7 +5,7 @@ import {JsonMapType} from '@shopify/cli-kit/node/toml' import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' export type DevSessionCreateMutationVariables = Types.Exact<{ - appId: Types.Scalars['String']['input'] + clientId: Types.Scalars['String']['input'] assetsUrl: Types.Scalars['String']['input'] websocketUrl?: Types.InputMaybe unsafeValidation?: Types.InputMaybe @@ -34,7 +34,7 @@ export const DevSessionCreate = { variableDefinitions: [ { kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + variable: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, }, { @@ -62,8 +62,8 @@ export const DevSessionCreate = { arguments: [ { kind: 'Argument', - name: {kind: 'Name', value: 'appId'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + name: {kind: 'Name', value: 'clientId'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, }, { kind: 'Argument', diff --git a/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-delete.ts b/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-delete.ts index 3f6ccb87cae..7065ef04128 100644 --- a/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-delete.ts +++ b/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-delete.ts @@ -4,7 +4,7 @@ import * as Types from './types.js' import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' export type DevSessionDeleteMutationVariables = Types.Exact<{ - appId: Types.Scalars['String']['input'] + clientId: Types.Scalars['String']['input'] }> export type DevSessionDeleteMutation = {devSessionDelete?: {userErrors: {message: string}[]} | null} @@ -19,7 +19,7 @@ export const DevSessionDelete = { variableDefinitions: [ { kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + variable: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, }, ], @@ -32,8 +32,8 @@ export const DevSessionDelete = { arguments: [ { kind: 'Argument', - name: {kind: 'Name', value: 'appId'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + name: {kind: 'Name', value: 'clientId'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, }, ], selectionSet: { diff --git a/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-update.ts b/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-update.ts index 12862dc837e..91caf916b18 100644 --- a/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-update.ts +++ b/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-update.ts @@ -5,7 +5,7 @@ import {JsonMapType} from '@shopify/cli-kit/node/toml' import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' export type DevSessionUpdateMutationVariables = Types.Exact<{ - appId: Types.Scalars['String']['input'] + clientId: Types.Scalars['String']['input'] assetsUrl?: Types.InputMaybe manifest?: Types.InputMaybe inheritedModuleUids: Types.Scalars['String']['input'][] | Types.Scalars['String']['input'] @@ -35,7 +35,7 @@ export const DevSessionUpdate = { variableDefinitions: [ { kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + variable: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, }, { @@ -74,8 +74,8 @@ export const DevSessionUpdate = { arguments: [ { kind: 'Argument', - name: {kind: 'Name', value: 'appId'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + name: {kind: 'Name', value: 'clientId'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, }, { kind: 'Argument', diff --git a/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-create.graphql b/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-create.graphql index 0921b45aa56..4e726846604 100644 --- a/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-create.graphql +++ b/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-create.graphql @@ -1,5 +1,5 @@ -mutation DevSessionCreate($appId: String!, $assetsUrl: String!, $websocketUrl: String, $unsafeValidation: Boolean) { - devSessionCreate(appId: $appId, assetsUrl: $assetsUrl, websocketUrl: $websocketUrl, isUnsafe: $unsafeValidation) { +mutation DevSessionCreate($clientId: String!, $assetsUrl: String!, $websocketUrl: String, $unsafeValidation: Boolean) { + devSessionCreate(clientId: $clientId, assetsUrl: $assetsUrl, websocketUrl: $websocketUrl, isUnsafe: $unsafeValidation) { devSession { websocketUrl updatedAt diff --git a/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-delete.graphql b/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-delete.graphql index c1a0700a5fb..a282504116b 100644 --- a/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-delete.graphql +++ b/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-delete.graphql @@ -1,5 +1,5 @@ -mutation DevSessionDelete($appId: String!) { - devSessionDelete(appId: $appId) { +mutation DevSessionDelete($clientId: String!) { + devSessionDelete(clientId: $clientId) { userErrors { message } diff --git a/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-update.graphql b/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-update.graphql index 6542832054b..b3cbd63f787 100644 --- a/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-update.graphql +++ b/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-update.graphql @@ -1,12 +1,12 @@ mutation DevSessionUpdate( - $appId: String! + $clientId: String! $assetsUrl: String $manifest: JSON $inheritedModuleUids: [String!]! $unsafeValidation: Boolean ) { devSessionUpdate( - appId: $appId + clientId: $clientId assetsUrl: $assetsUrl manifest: $manifest inheritedModuleUids: $inheritedModuleUids diff --git a/packages/app/src/cli/api/graphql/app-management/generated/active-app-release.ts b/packages/app/src/cli/api/graphql/app-management/generated/active-app-release.ts index f4c874b90ab..86bf4bae0b1 100644 --- a/packages/app/src/cli/api/graphql/app-management/generated/active-app-release.ts +++ b/packages/app/src/cli/api/graphql/app-management/generated/active-app-release.ts @@ -1,42 +1,8 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import * as Types from './types.js' import {JsonMapType} from '@shopify/cli-kit/node/toml' import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' -export type ActiveAppReleaseQueryVariables = Types.Exact<{ - appId: Types.Scalars['ID']['input'] -}> - -export type ActiveAppReleaseQuery = { - app: { - id: string - key: string - organizationId: string - activeRoot: {grantedShopifyApprovalScopes: string[]; clientCredentials: {secrets: {key: string}[]}} - activeRelease: { - id: string - version: { - name: string - appModules: { - uuid: string - userIdentifier: string - handle: string - config: JsonMapType - target?: string | null - specification: { - identifier: string - externalIdentifier: string - name: string - experience: string - managementExperience: string - } - }[] - } - } - } -} - export type AppVersionInfoFragment = { id: string key: string @@ -215,140 +181,3 @@ export const AppVersionInfoFragmentDoc = { }, ], } as unknown as DocumentNode -export const ActiveAppRelease = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'query', - name: {kind: 'Name', value: 'activeAppRelease'}, - variableDefinitions: [ - { - kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, - type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}}, - }, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: {kind: 'Name', value: 'app'}, - arguments: [ - { - kind: 'Argument', - name: {kind: 'Name', value: 'id'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, - }, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - {kind: 'FragmentSpread', name: {kind: 'Name', value: 'AppVersionInfo'}}, - {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, - ], - }, - }, - ], - }, - }, - { - kind: 'FragmentDefinition', - name: {kind: 'Name', value: 'ReleasedAppModule'}, - typeCondition: {kind: 'NamedType', name: {kind: 'Name', value: 'AppModule'}}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - {kind: 'Field', name: {kind: 'Name', value: 'uuid'}}, - {kind: 'Field', name: {kind: 'Name', value: 'userIdentifier'}}, - {kind: 'Field', name: {kind: 'Name', value: 'handle'}}, - {kind: 'Field', name: {kind: 'Name', value: 'config'}}, - {kind: 'Field', name: {kind: 'Name', value: 'target'}}, - { - kind: 'Field', - name: {kind: 'Name', value: 'specification'}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - {kind: 'Field', name: {kind: 'Name', value: 'identifier'}}, - {kind: 'Field', name: {kind: 'Name', value: 'externalIdentifier'}}, - {kind: 'Field', name: {kind: 'Name', value: 'name'}}, - {kind: 'Field', name: {kind: 'Name', value: 'experience'}}, - {kind: 'Field', name: {kind: 'Name', value: 'managementExperience'}}, - ], - }, - }, - ], - }, - }, - { - kind: 'FragmentDefinition', - name: {kind: 'Name', value: 'AppVersionInfo'}, - typeCondition: {kind: 'NamedType', name: {kind: 'Name', value: 'App'}}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - {kind: 'Field', name: {kind: 'Name', value: 'id'}}, - {kind: 'Field', name: {kind: 'Name', value: 'key'}}, - {kind: 'Field', name: {kind: 'Name', value: 'organizationId'}}, - { - kind: 'Field', - name: {kind: 'Name', value: 'activeRoot'}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: {kind: 'Name', value: 'clientCredentials'}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: {kind: 'Name', value: 'secrets'}, - selectionSet: { - kind: 'SelectionSet', - selections: [{kind: 'Field', name: {kind: 'Name', value: 'key'}}], - }, - }, - ], - }, - }, - {kind: 'Field', name: {kind: 'Name', value: 'grantedShopifyApprovalScopes'}}, - ], - }, - }, - { - kind: 'Field', - name: {kind: 'Name', value: 'activeRelease'}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - {kind: 'Field', name: {kind: 'Name', value: 'id'}}, - { - kind: 'Field', - name: {kind: 'Name', value: 'version'}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - {kind: 'Field', name: {kind: 'Name', value: 'name'}}, - { - kind: 'Field', - name: {kind: 'Name', value: 'appModules'}, - selectionSet: { - kind: 'SelectionSet', - selections: [{kind: 'FragmentSpread', name: {kind: 'Name', value: 'ReleasedAppModule'}}], - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - ], -} as unknown as DocumentNode diff --git a/packages/app/src/cli/api/graphql/app-management/generated/app-install-count.ts b/packages/app/src/cli/api/graphql/app-management/generated/app-install-count.ts index b9a478c01a2..fd110a4fd50 100644 --- a/packages/app/src/cli/api/graphql/app-management/generated/app-install-count.ts +++ b/packages/app/src/cli/api/graphql/app-management/generated/app-install-count.ts @@ -4,7 +4,7 @@ import * as Types from './types.js' import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' export type AppInstallCountQueryVariables = Types.Exact<{ - appId: Types.Scalars['ID']['input'] + clientId: Types.Scalars['String']['input'] }> export type AppInstallCountQuery = {app: {installCount?: number | null}} @@ -19,8 +19,8 @@ export const AppInstallCount = { variableDefinitions: [ { kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, - type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}}, + variable: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, }, ], selectionSet: { @@ -28,12 +28,13 @@ export const AppInstallCount = { selections: [ { kind: 'Field', - name: {kind: 'Name', value: 'app'}, + alias: {kind: 'Name', value: 'app'}, + name: {kind: 'Name', value: 'appByKey'}, arguments: [ { kind: 'Argument', - name: {kind: 'Name', value: 'id'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + name: {kind: 'Name', value: 'key'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, }, ], selectionSet: { diff --git a/packages/app/src/cli/api/graphql/app-management/generated/app-version-by-tag.ts b/packages/app/src/cli/api/graphql/app-management/generated/app-version-by-tag.ts index f54e9d660da..3e667dfdbe8 100644 --- a/packages/app/src/cli/api/graphql/app-management/generated/app-version-by-tag.ts +++ b/packages/app/src/cli/api/graphql/app-management/generated/app-version-by-tag.ts @@ -5,6 +5,7 @@ import {JsonMapType} from '@shopify/cli-kit/node/toml' import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' export type AppVersionByTagQueryVariables = Types.Exact<{ + clientId: Types.Scalars['String']['input'] versionTag: Types.Scalars['String']['input'] }> @@ -37,6 +38,11 @@ export const AppVersionByTag = { operation: 'query', name: {kind: 'Name', value: 'AppVersionByTag'}, variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, + }, { kind: 'VariableDefinition', variable: {kind: 'Variable', name: {kind: 'Name', value: 'versionTag'}}, @@ -50,6 +56,11 @@ export const AppVersionByTag = { kind: 'Field', name: {kind: 'Name', value: 'versionByTag'}, arguments: [ + { + kind: 'Argument', + name: {kind: 'Name', value: 'clientId'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, + }, { kind: 'Argument', name: {kind: 'Name', value: 'tag'}, diff --git a/packages/app/src/cli/api/graphql/app-management/generated/app-versions.ts b/packages/app/src/cli/api/graphql/app-management/generated/app-versions.ts index 38ce32f74fc..26d200bdb5a 100644 --- a/packages/app/src/cli/api/graphql/app-management/generated/app-versions.ts +++ b/packages/app/src/cli/api/graphql/app-management/generated/app-versions.ts @@ -4,7 +4,7 @@ import * as Types from './types.js' import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' export type AppVersionsQueryVariables = Types.Exact<{ - appId: Types.Scalars['ID']['input'] + clientId: Types.Scalars['String']['input'] }> export type AppVersionsQuery = { @@ -35,8 +35,8 @@ export const AppVersions = { variableDefinitions: [ { kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, - type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}}, + variable: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, }, ], selectionSet: { @@ -44,12 +44,13 @@ export const AppVersions = { selections: [ { kind: 'Field', - name: {kind: 'Name', value: 'app'}, + alias: {kind: 'Name', value: 'app'}, + name: {kind: 'Name', value: 'appByKey'}, arguments: [ { kind: 'Argument', - name: {kind: 'Name', value: 'id'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + name: {kind: 'Name', value: 'key'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, }, ], selectionSet: { diff --git a/packages/app/src/cli/api/graphql/app-management/generated/create-app-version.ts b/packages/app/src/cli/api/graphql/app-management/generated/create-app-version.ts index 51cf3217c09..ffe150a7b8f 100644 --- a/packages/app/src/cli/api/graphql/app-management/generated/create-app-version.ts +++ b/packages/app/src/cli/api/graphql/app-management/generated/create-app-version.ts @@ -5,7 +5,7 @@ import {JsonMapType} from '@shopify/cli-kit/node/toml' import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' export type CreateAppVersionMutationVariables = Types.Exact<{ - appId: Types.Scalars['ID']['input'] + clientId: Types.Scalars['String']['input'] version: Types.AppVersionInput metadata?: Types.InputMaybe }> @@ -50,8 +50,8 @@ export const CreateAppVersion = { variableDefinitions: [ { kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, - type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}}, + variable: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, }, { kind: 'VariableDefinition', @@ -73,8 +73,8 @@ export const CreateAppVersion = { arguments: [ { kind: 'Argument', - name: {kind: 'Name', value: 'appId'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + name: {kind: 'Name', value: 'clientId'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, }, { kind: 'Argument', diff --git a/packages/app/src/cli/api/graphql/app-management/generated/create-source-scan.ts b/packages/app/src/cli/api/graphql/app-management/generated/create-source-scan.ts index 37e969c5b0b..357476dfaf0 100644 --- a/packages/app/src/cli/api/graphql/app-management/generated/create-source-scan.ts +++ b/packages/app/src/cli/api/graphql/app-management/generated/create-source-scan.ts @@ -4,7 +4,7 @@ import * as Types from './types.js' import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' export type CreateSourceScanMutationVariables = Types.Exact<{ - appId: Types.Scalars['ID']['input'] + clientId: Types.Scalars['String']['input'] sourceScanUrl: Types.Scalars['URL']['input'] }> @@ -22,8 +22,8 @@ export const CreateSourceScan = { variableDefinitions: [ { kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, - type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}}, + variable: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, }, { kind: 'VariableDefinition', @@ -40,8 +40,8 @@ export const CreateSourceScan = { arguments: [ { kind: 'Argument', - name: {kind: 'Name', value: 'appId'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + name: {kind: 'Name', value: 'clientId'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, }, { kind: 'Argument', diff --git a/packages/app/src/cli/api/graphql/app-management/generated/release-version.ts b/packages/app/src/cli/api/graphql/app-management/generated/release-version.ts index cc5fea2e080..6411ad9d6aa 100644 --- a/packages/app/src/cli/api/graphql/app-management/generated/release-version.ts +++ b/packages/app/src/cli/api/graphql/app-management/generated/release-version.ts @@ -5,7 +5,7 @@ import {JsonMapType} from '@shopify/cli-kit/node/toml' import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' export type ReleaseVersionMutationVariables = Types.Exact<{ - appId: Types.Scalars['ID']['input'] + clientId: Types.Scalars['String']['input'] versionId: Types.Scalars['ID']['input'] }> @@ -32,8 +32,8 @@ export const ReleaseVersion = { variableDefinitions: [ { kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, - type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}}, + variable: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, }, { kind: 'VariableDefinition', @@ -50,8 +50,8 @@ export const ReleaseVersion = { arguments: [ { kind: 'Argument', - name: {kind: 'Name', value: 'appId'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + name: {kind: 'Name', value: 'clientId'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, }, { kind: 'Argument', diff --git a/packages/app/src/cli/api/graphql/app-management/generated/request-source-scan-upload-url.ts b/packages/app/src/cli/api/graphql/app-management/generated/request-source-scan-upload-url.ts index 322c0a477d8..cb8f0c1ac1b 100644 --- a/packages/app/src/cli/api/graphql/app-management/generated/request-source-scan-upload-url.ts +++ b/packages/app/src/cli/api/graphql/app-management/generated/request-source-scan-upload-url.ts @@ -4,7 +4,7 @@ import * as Types from './types.js' import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' export type RequestSourceScanUploadUrlMutationVariables = Types.Exact<{ - appId: Types.Scalars['ID']['input'] + clientId: Types.Scalars['String']['input'] byteSize: Types.Scalars['Int']['input'] }> @@ -25,8 +25,8 @@ export const RequestSourceScanUploadUrl = { variableDefinitions: [ { kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, - type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}}, + variable: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}}, }, { kind: 'VariableDefinition', @@ -43,8 +43,8 @@ export const RequestSourceScanUploadUrl = { arguments: [ { kind: 'Argument', - name: {kind: 'Name', value: 'appId'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + name: {kind: 'Name', value: 'clientId'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'clientId'}}, }, { kind: 'Argument', diff --git a/packages/app/src/cli/api/graphql/app-management/queries/active-app-release.graphql b/packages/app/src/cli/api/graphql/app-management/queries/active-app-release.graphql index 4bf5b82d8d9..1540025fff5 100644 --- a/packages/app/src/cli/api/graphql/app-management/queries/active-app-release.graphql +++ b/packages/app/src/cli/api/graphql/app-management/queries/active-app-release.graphql @@ -1,9 +1,3 @@ -query activeAppRelease($appId: ID!) { - app(id: $appId) { - ...AppVersionInfo - } -} - fragment AppVersionInfo on App { id key diff --git a/packages/app/src/cli/api/graphql/app-management/queries/app-install-count.graphql b/packages/app/src/cli/api/graphql/app-management/queries/app-install-count.graphql index e5495a3fff4..68d0dc66508 100644 --- a/packages/app/src/cli/api/graphql/app-management/queries/app-install-count.graphql +++ b/packages/app/src/cli/api/graphql/app-management/queries/app-install-count.graphql @@ -1,5 +1,5 @@ -query AppInstallCount($appId: ID!) { - app(id: $appId) { +query AppInstallCount($clientId: String!) { + app: appByKey(key: $clientId) { installCount } } diff --git a/packages/app/src/cli/api/graphql/app-management/queries/app-version-by-tag.graphql b/packages/app/src/cli/api/graphql/app-management/queries/app-version-by-tag.graphql index 22207aec320..53707fcfaad 100644 --- a/packages/app/src/cli/api/graphql/app-management/queries/app-version-by-tag.graphql +++ b/packages/app/src/cli/api/graphql/app-management/queries/app-version-by-tag.graphql @@ -1,5 +1,5 @@ -query AppVersionByTag($versionTag: String!) { - versionByTag(tag: $versionTag) { +query AppVersionByTag($clientId: String!, $versionTag: String!) { + versionByTag(clientId: $clientId, tag: $versionTag) { ...VersionInfo } } diff --git a/packages/app/src/cli/api/graphql/app-management/queries/app-versions.graphql b/packages/app/src/cli/api/graphql/app-management/queries/app-versions.graphql index 3917f18c2ae..41706364809 100644 --- a/packages/app/src/cli/api/graphql/app-management/queries/app-versions.graphql +++ b/packages/app/src/cli/api/graphql/app-management/queries/app-versions.graphql @@ -1,5 +1,5 @@ -query AppVersions($appId: ID!) { - app(id: $appId) { +query AppVersions($clientId: String!) { + app: appByKey(key: $clientId) { id activeRelease { id diff --git a/packages/app/src/cli/api/graphql/app-management/queries/create-app-version.graphql b/packages/app/src/cli/api/graphql/app-management/queries/create-app-version.graphql index 1a222ee26be..b2ec7da0f33 100644 --- a/packages/app/src/cli/api/graphql/app-management/queries/create-app-version.graphql +++ b/packages/app/src/cli/api/graphql/app-management/queries/create-app-version.graphql @@ -1,5 +1,5 @@ -mutation CreateAppVersion($appId: ID!, $version: AppVersionInput!, $metadata: VersionMetadataInput) { - appVersionCreate(appId: $appId, version: $version, metadata: $metadata) { +mutation CreateAppVersion($clientId: String!, $version: AppVersionInput!, $metadata: VersionMetadataInput) { + appVersionCreate(clientId: $clientId, version: $version, metadata: $metadata) { version { id appModules { diff --git a/packages/app/src/cli/api/graphql/app-management/queries/create-source-scan.graphql b/packages/app/src/cli/api/graphql/app-management/queries/create-source-scan.graphql index e59232c70da..94f8f716baf 100644 --- a/packages/app/src/cli/api/graphql/app-management/queries/create-source-scan.graphql +++ b/packages/app/src/cli/api/graphql/app-management/queries/create-source-scan.graphql @@ -1,5 +1,5 @@ -mutation CreateSourceScan($appId: ID!, $sourceScanUrl: URL!) { - appSourceScanCreate(appId: $appId, sourceScanUrl: $sourceScanUrl) { +mutation CreateSourceScan($clientId: String!, $sourceScanUrl: URL!) { + appSourceScanCreate(clientId: $clientId, sourceScanUrl: $sourceScanUrl) { accepted userErrors { field diff --git a/packages/app/src/cli/api/graphql/app-management/queries/release-version.graphql b/packages/app/src/cli/api/graphql/app-management/queries/release-version.graphql index b7dfb6de743..8f9ad48216f 100644 --- a/packages/app/src/cli/api/graphql/app-management/queries/release-version.graphql +++ b/packages/app/src/cli/api/graphql/app-management/queries/release-version.graphql @@ -1,5 +1,5 @@ -mutation ReleaseVersion($appId: ID!, $versionId: ID!) { - appReleaseCreate(appId: $appId, versionId: $versionId) { +mutation ReleaseVersion($clientId: String!, $versionId: ID!) { + appReleaseCreate(clientId: $clientId, versionId: $versionId) { release { version { id diff --git a/packages/app/src/cli/api/graphql/app-management/queries/request-source-scan-upload-url.graphql b/packages/app/src/cli/api/graphql/app-management/queries/request-source-scan-upload-url.graphql index e01884eb654..92550c99b6e 100644 --- a/packages/app/src/cli/api/graphql/app-management/queries/request-source-scan-upload-url.graphql +++ b/packages/app/src/cli/api/graphql/app-management/queries/request-source-scan-upload-url.graphql @@ -1,5 +1,5 @@ -mutation RequestSourceScanUploadUrl($appId: ID!, $byteSize: Int!) { - appRequestSourceScanUploadUrl(appId: $appId, byteSize: $byteSize) { +mutation RequestSourceScanUploadUrl($clientId: String!, $byteSize: Int!) { + appRequestSourceScanUploadUrl(clientId: $clientId, byteSize: $byteSize) { sourceScanUploadUrl userErrors { field diff --git a/packages/app/src/cli/commands/app/security/submit.integration.test.ts b/packages/app/src/cli/commands/app/security/submit.integration.test.ts index bdb03972e04..b4f7e87655d 100644 --- a/packages/app/src/cli/commands/app/security/submit.integration.test.ts +++ b/packages/app/src/cli/commands/app/security/submit.integration.test.ts @@ -280,7 +280,14 @@ describe('app security submit command boundary', () => { 'slow-request', ) expect(JSON.parse(writtenBytes.toString()).report.feedback).toBe('src/private.ts secret') - expect(client.generateSourceScanUploadUrl).toHaveBeenCalledWith({appId: '1', byteSize: writtenBytes.length}) + expect(client.generateSourceScanUploadUrl).toHaveBeenCalledWith({ + clientId: 'api-key', + byteSize: writtenBytes.length, + }) + expect(client.createSourceScan).toHaveBeenCalledWith({ + clientId: 'api-key', + sourceScanUrl: signedUploadUrl, + }) await expect(readdir(joinPath(directory, '.shopify'))).resolves.toEqual(['app-security']) }) }) diff --git a/packages/app/src/cli/services/app-security-submit-api.test.ts b/packages/app/src/cli/services/app-security-submit-api.test.ts index 331aabae7aa..c863c833919 100644 --- a/packages/app/src/cli/services/app-security-submit-api.test.ts +++ b/packages/app/src/cli/services/app-security-submit-api.test.ts @@ -189,7 +189,10 @@ describe('submitAppSecurityScan', () => { await expect(submitAppSecurityScan(input, {upload})).resolves.toEqual({status: 'submitted'}) - expect(generateSourceScanUploadUrl).toHaveBeenCalledWith({appId: app.id, byteSize: input.payload.bytes.length}) + expect(generateSourceScanUploadUrl).toHaveBeenCalledWith({ + clientId: app.apiKey, + byteSize: input.payload.bytes.length, + }) expect(generateSourceScanUploadUrl.mock.invocationCallOrder[0]).toBeLessThan(upload.mock.invocationCallOrder[0]!) expect(upload).toHaveBeenCalledOnce() const [url, bytes, uploadOptions] = upload.mock.calls[0]! @@ -197,7 +200,7 @@ describe('submitAppSecurityScan', () => { expect(bytes).toBe(input.payload.bytes) expect(uploadOptions).toEqual({artifactName: 'App Security submission', contentType: 'application/json'}) expect(createSourceScan).toHaveBeenCalledWith({ - appId: app.id, + clientId: app.apiKey, sourceScanUrl: 'source-scan-upload-url', }) }) diff --git a/packages/app/src/cli/services/app-security-submit-api.ts b/packages/app/src/cli/services/app-security-submit-api.ts index 4ad4184f91c..81a6b483e05 100644 --- a/packages/app/src/cli/services/app-security-submit-api.ts +++ b/packages/app/src/cli/services/app-security-submit-api.ts @@ -28,7 +28,7 @@ export async function submitAppSecurityScan( let stage: SecuritySubmitError['stage'] = 'upload-url' try { const uploadResult = await options.developerPlatformClient.generateSourceScanUploadUrl({ - appId: options.app.id, + clientId: options.app.apiKey, byteSize: options.payload.bytes.length, }) if (!uploadResult.sourceScanUploadUrl || uploadResult.userErrors.length > 0) { @@ -50,7 +50,7 @@ export async function submitAppSecurityScan( stage = 'create' const createResult = await options.developerPlatformClient.createSourceScan({ - appId: options.app.id, + clientId: options.app.apiKey, sourceScanUrl: uploadResult.sourceScanUploadUrl, }) if (createResult.userErrors.length > 0 || !createResult.accepted) { diff --git a/packages/app/src/cli/services/dev-clean.test.ts b/packages/app/src/cli/services/dev-clean.test.ts index 3d8fe124c37..c0998f16530 100644 --- a/packages/app/src/cli/services/dev-clean.test.ts +++ b/packages/app/src/cli/services/dev-clean.test.ts @@ -12,7 +12,7 @@ const mockStore = testOrganizationStore({shopDomain}) const mockOptions = { appContextResult: { developerPlatformClient: testDeveloperPlatformClient(), - remoteApp: {id: 'app-id-1', title: 'Test App', apiKey: 'api-key-1'}, + remoteApp: {id: 'gid://shopify/App/1', title: 'Test App', apiKey: 'client-id-1'}, } as unknown as LoadedAppContextOutput, store: mockStore, } @@ -26,6 +26,10 @@ describe('devClean', () => { await devClean(mockOptions) // Then + expect(mockOptions.appContextResult.developerPlatformClient.devSessionDelete).toHaveBeenCalledWith({ + clientId: 'client-id-1', + shopFqdn: mockStore.shopDomain, + }) expect(renderSuccess).toHaveBeenCalledWith({ headline: 'Dev preview stopped.', body: [ diff --git a/packages/app/src/cli/services/dev-clean.ts b/packages/app/src/cli/services/dev-clean.ts index ee3118b365e..c8d27f1f95b 100644 --- a/packages/app/src/cli/services/dev-clean.ts +++ b/packages/app/src/cli/services/dev-clean.ts @@ -12,7 +12,7 @@ export async function devClean(options: DevCleanOptions) { const client = options.appContextResult.developerPlatformClient const remoteApp = options.appContextResult.remoteApp - const result = await client.devSessionDelete({shopFqdn: options.store.shopDomain, appId: remoteApp.id}) + const result = await client.devSessionDelete({shopFqdn: options.store.shopDomain, clientId: remoteApp.apiKey}) if (result.devSessionDelete?.userErrors.length) { const errors = result.devSessionDelete.userErrors.map((error) => error.message).join('\n') diff --git a/packages/app/src/cli/services/dev.ts b/packages/app/src/cli/services/dev.ts index e5a81c4ad65..b9cd43fa9f4 100644 --- a/packages/app/src/cli/services/dev.ts +++ b/packages/app/src/cli/services/dev.ts @@ -341,7 +341,7 @@ async function launchDevProcesses({ const developerPlatformClient = config.developerPlatformClient const app = { - id: config.remoteApp.id, + apiKey: config.remoteApp.apiKey, developerPlatformClient, } diff --git a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.test.ts b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.test.ts index 70ee061db60..decc60bc8f6 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.test.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.test.ts @@ -37,7 +37,7 @@ describe('setupDevSessionProcess', () => { storeFqdn: 'test.myshopify.com', url: 'https://test.dev', organizationId: 'org123', - appId: 'app123', + appId: 'gid://shopify/App/123', appWatcher: {} as AppEventWatcher, appPreviewURL: 'https://test.preview.url', devSessionStatusManager: new DevSessionStatusManager(), @@ -93,7 +93,7 @@ describe('pushUpdatesForDevSession', () => { appWatcher, storeFqdn: 'test.myshopify.com', url: 'https://test.dev', - appId: 'app123', + appId: 'gid://shopify/App/123', organizationId: 'org123', appPreviewURL: 'https://test.preview.url', devSessionStatusManager, @@ -443,6 +443,8 @@ describe('pushUpdatesForDevSession', () => { test('manifest sent in update payload only includes affected extensions', async () => { // Given + options.apiKey = 'client-id-123' + options.organizationId = '5' vi.mocked(readdir).mockResolvedValue(['assets', 'assets/updated-extension']) vi.mocked(getUploadURL).mockResolvedValue('https://gcs.url') @@ -466,7 +468,7 @@ describe('pushUpdatesForDevSession', () => { // Then expect(developerPlatformClient.devSessionUpdate).toHaveBeenCalledWith({ shopFqdn: 'test.myshopify.com', - appId: 'app123', + clientId: 'client-id-123', // Assets URL is empty because the affected extension has no assets assetsUrl: undefined, manifest: { @@ -594,7 +596,7 @@ describe('pushUpdatesForDevSession', () => { // Then expect(developerPlatformClient.devSessionUpdate).toHaveBeenCalledWith({ shopFqdn: 'test.myshopify.com', - appId: 'app123', + clientId: 'test-api-key', assetsUrl: 'https://gcs.url', manifest: expect.any(Object), inheritedModuleUids: [], @@ -604,6 +606,8 @@ describe('pushUpdatesForDevSession', () => { test('assetsURL is always generated for create, even if there are no assets', async () => { // Given + options.apiKey = 'client-id-123' + options.organizationId = '5' vi.mocked(formData).mockReturnValue({append: vi.fn(), getHeaders: vi.fn()} as any) vi.mocked(getUploadURL).mockResolvedValue('https://gcs.url') @@ -615,11 +619,16 @@ describe('pushUpdatesForDevSession', () => { // Then expect(developerPlatformClient.devSessionCreate).toHaveBeenCalledWith({ shopFqdn: 'test.myshopify.com', - appId: 'app123', + clientId: 'client-id-123', assetsUrl: 'https://gcs.url', websocketUrl: 'wss://test.dev/extensions', unsafeValidation: false, }) + expect(getUploadURL).toHaveBeenCalledWith(developerPlatformClient, { + apiKey: 'gid://shopify/App/123', + organizationId: '5', + id: 'gid://shopify/App/123', + }) }) test('multiple updates to different extensions are consolidated if a request is in progress', async () => { diff --git a/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts b/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts index d799ddf127f..ef0cb75e7b1 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts @@ -304,7 +304,7 @@ export class DevSession { if (this.statusManager.status.isReady) { const payload: DevSessionUpdateOptions = { shopFqdn: this.options.storeFqdn, - appId: this.options.appId, + clientId: this.options.apiKey, assetsUrl: signedURL, manifest, inheritedModuleUids, @@ -314,7 +314,7 @@ export class DevSession { } else { const payload: DevSessionCreateOptions = { shopFqdn: this.options.storeFqdn, - appId: this.options.appId, + clientId: this.options.apiKey, assetsUrl: signedURL, websocketUrl, unsafeValidation: this.options.unsafeValidation ?? false, diff --git a/packages/app/src/cli/services/dev/ui.test.tsx b/packages/app/src/cli/services/dev/ui.test.tsx index ad6e27c29e8..2644c8ea4c5 100644 --- a/packages/app/src/cli/services/dev/ui.test.tsx +++ b/packages/app/src/cli/services/dev/ui.test.tsx @@ -31,7 +31,7 @@ describe('ui', () => { processes: [concurrentProcess], previewUrl: 'https://lala.cloudflare.io/', graphiqlUrl: 'https://lala.cloudflare.io/graphiql', - app: {id: '123', developerPlatformClient}, + app: {apiKey: 'client-id-123', developerPlatformClient}, abortController, shopFqdn: 'mystore.shopify.io', devSessionStatusManager, @@ -57,7 +57,7 @@ describe('ui', () => { processes: [concurrentProcess], previewUrl: 'https://lala.cloudflare.io/', graphiqlUrl: 'https://lala.cloudflare.io/graphiql', - app: {id: '123', developerPlatformClient}, + app: {apiKey: 'client-id-123', developerPlatformClient}, abortController: new AbortController(), shopFqdn: 'mystore.shopify.io', devSessionStatusManager, @@ -84,7 +84,7 @@ describe('ui', () => { previewUrl: 'https://lala.cloudflare.io/', graphiqlUrl: 'https://lala.cloudflare.io/graphiql', app: { - id: '123', + apiKey: 'client-id-123', developerPlatformClient: { ...developerPlatformClient, devSessionDelete: vi.fn(), @@ -113,7 +113,7 @@ describe('ui', () => { test('calls devSessionDelete when DevSessionUI aborts', async () => { vi.mocked(terminalSupportsPrompting).mockReturnValue(true) const app = { - id: '123', + apiKey: 'client-id-123', developerPlatformClient: { ...developerPlatformClient, devSessionDelete: vi.fn(), @@ -143,7 +143,7 @@ describe('ui', () => { await onAbort?.() expect(app.developerPlatformClient.devSessionDelete).toHaveBeenCalledWith({ - appId: app.id, + clientId: app.apiKey, shopFqdn, }) }) diff --git a/packages/app/src/cli/services/dev/ui.tsx b/packages/app/src/cli/services/dev/ui.tsx index 9eb23042433..7cea742cbdf 100644 --- a/packages/app/src/cli/services/dev/ui.tsx +++ b/packages/app/src/cli/services/dev/ui.tsx @@ -14,7 +14,7 @@ interface DevProps { abortController: AbortController shopFqdn: string app: { - id: string + apiKey: string developerPlatformClient: DeveloperPlatformClient } } @@ -53,7 +53,7 @@ export async function renderDev({ configPath={configPath} localURL={localURL} onAbort={async () => { - await app.developerPlatformClient.devSessionDelete({appId: app.id, shopFqdn}) + await app.developerPlatformClient.devSessionDelete({clientId: app.apiKey, shopFqdn}) }} />, { diff --git a/packages/app/src/cli/utilities/developer-platform-client.ts b/packages/app/src/cli/utilities/developer-platform-client.ts index ac22e68a187..a903280e8ac 100644 --- a/packages/app/src/cli/utilities/developer-platform-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client.ts @@ -115,7 +115,7 @@ export type AppDeployOptions = AppDeployVariables & { interface DevSessionSharedOptions { shopFqdn: string - appId: string + clientId: string } export interface DevSessionCreateOptions extends DevSessionSharedOptions { @@ -149,12 +149,12 @@ export type SourceScanUploadUrlSchema = WithUserErrors<{ }> export interface SourceScanUploadUrlInput { - appId: string + clientId: string byteSize: number } export interface SourceScanCreateInput { - appId: string + clientId: string sourceScanUrl: string } diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts index 47c085ac351..e3562e283a7 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts @@ -35,8 +35,16 @@ import {PublicApiVersionsQuery} from '../../api/graphql/webhooks/generated/publi import {AvailableTopicsQuery} from '../../api/graphql/webhooks/generated/available-topics.js' import {CliTesting, CliTestingMutation} from '../../api/graphql/webhooks/generated/cli-testing.js' import {SendSampleWebhookVariables} from '../../services/webhook/request-sample.js' +import {getUploadURL} from '../../services/bundle.js' import {CreateApp} from '../../api/graphql/app-management/generated/create-app.js' import {AppVersions, AppVersionsQuery} from '../../api/graphql/app-management/generated/app-versions.js' +import {AppInstallCount} from '../../api/graphql/app-management/generated/app-install-count.js' +import {AppVersionByTag} from '../../api/graphql/app-management/generated/app-version-by-tag.js' +import {CreateAppVersion} from '../../api/graphql/app-management/generated/create-app-version.js' +import {ReleaseVersion} from '../../api/graphql/app-management/generated/release-version.js' +import {DevSessionCreate} from '../../api/graphql/app-dev/generated/dev-session-create.js' +import {DevSessionUpdate} from '../../api/graphql/app-dev/generated/dev-session-update.js' +import {DevSessionDelete} from '../../api/graphql/app-dev/generated/dev-session-delete.js' import {AppVersionsQuerySchema} from '../../api/graphql/get_versions_list.js' import {BrandingSpecIdentifier} from '../../models/extensions/specifications/app_config_branding.js' import {AppHomeSpecIdentifier} from '../../models/extensions/specifications/app_config_app_home.js' @@ -46,6 +54,7 @@ import {CreateAssetUrl} from '../../api/graphql/app-management/generated/create- import {RequestSourceScanUploadUrl} from '../../api/graphql/app-management/generated/request-source-scan-upload-url.js' import {CreateSourceScan} from '../../api/graphql/app-management/generated/create-source-scan.js' import {SourceExtension} from '../../api/graphql/app-management/generated/types.js' +import {print} from 'graphql' import {fetchOrganizationById, fetchOrganizations} from '@shopify/organizations' import {describe, expect, test, vi, beforeEach} from 'vitest' import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version' @@ -1021,34 +1030,90 @@ describe('sendSampleWebhook', () => { }) }) +describe('client ID documents', () => { + test.each([ + ['create version', CreateAppVersion], + ['release version', ReleaseVersion], + ['request source scan upload URL', RequestSourceScanUploadUrl], + ['create source scan', CreateSourceScan], + ['find version by tag', AppVersionByTag], + ['create dev session', DevSessionCreate], + ['update dev session', DevSessionUpdate], + ['delete dev session', DevSessionDelete], + ])('%s declares and sends only the required client ID', (_operation, document) => { + const query = print(document) + expect(query).toContain('$clientId: String!') + expect(query).toContain('clientId: $clientId') + expect(query).not.toMatch(/\bappId\b/) + }) + + test('nested app queries use the existing key lookup and retain their selected fields', () => { + const versionsQuery = print(AppVersions) + expect(versionsQuery).toContain('$clientId: String!') + expect(versionsQuery).toContain('app: appByKey(key: $clientId)') + expect(versionsQuery).toContain('versions(first: 20)') + expect(versionsQuery).toContain('activeRelease') + expect(versionsQuery).toContain('versionsCount') + expect(versionsQuery).not.toMatch(/\bappId\b/) + + const installCountQuery = print(AppInstallCount) + expect(installCountQuery).toContain('$clientId: String!') + expect(installCountQuery).toContain('app: appByKey(key: $clientId)') + expect(installCountQuery).toContain('installCount') + expect(installCountQuery).not.toMatch(/\bappId\b/) + }) +}) + describe('dev session requests', () => { test('sends the enabled unsafe validation value to create and update requests', async () => { const client = AppManagementClient.getInstance() client.token = () => Promise.resolve('token') + const clientId = 'client-id-123' + const manifest = {name: 'App', handle: 'app', modules: []} await client.devSessionCreate({ - appId: 'gid://shopify/App/123', + clientId, assetsUrl: 'https://assets.test', shopFqdn: 'test.myshopify.com', websocketUrl: 'wss://test.dev/extensions', unsafeValidation: true, }) await client.devSessionUpdate({ - appId: 'gid://shopify/App/123', + clientId, assetsUrl: 'https://assets.test', shopFqdn: 'test.myshopify.com', - manifest: {name: 'App', handle: 'app', modules: []}, - inheritedModuleUids: [], + manifest, + inheritedModuleUids: ['existing-module'], unsafeValidation: true, }) expect(appDevRequestDoc).toHaveBeenNthCalledWith( 1, - expect.objectContaining({variables: expect.objectContaining({unsafeValidation: true})}), + expect.objectContaining({ + query: DevSessionCreate, + shopFqdn: 'test.myshopify.com', + variables: { + clientId, + assetsUrl: 'https://assets.test', + websocketUrl: 'wss://test.dev/extensions', + unsafeValidation: true, + }, + requestOptions: {requestMode: 'slow-request'}, + }), ) expect(appDevRequestDoc).toHaveBeenNthCalledWith( 2, - expect.objectContaining({variables: expect.objectContaining({unsafeValidation: true})}), + expect.objectContaining({ + query: DevSessionUpdate, + shopFqdn: 'test.myshopify.com', + variables: { + clientId, + assetsUrl: 'https://assets.test', + manifest: JSON.stringify(manifest), + inheritedModuleUids: ['existing-module'], + unsafeValidation: true, + }, + }), ) }) @@ -1057,12 +1122,12 @@ describe('dev session requests', () => { client.token = () => Promise.resolve('token') await client.devSessionCreate({ - appId: 'gid://shopify/App/123', + clientId: 'client-id-123', assetsUrl: 'https://assets.test', shopFqdn: 'test.myshopify.com', }) await client.devSessionUpdate({ - appId: 'gid://shopify/App/123', + clientId: 'client-id-123', shopFqdn: 'test.myshopify.com', manifest: {name: 'App', handle: 'app', modules: []}, inheritedModuleUids: [], @@ -1077,6 +1142,23 @@ describe('dev session requests', () => { expect.objectContaining({variables: expect.objectContaining({unsafeValidation: false})}), ) }) + + test('deletes the dev session by client ID for the requested shop', async () => { + const client = AppManagementClient.getInstance() + client.token = () => Promise.resolve('token') + vi.mocked(appDevRequestDoc).mockResolvedValueOnce({devSessionDelete: {userErrors: []}}) + + const result = await client.devSessionDelete({clientId: 'client-id-123', shopFqdn: 'test.myshopify.com'}) + + expect(result).toEqual({devSessionDelete: {userErrors: []}}) + expect(appDevRequestDoc).toHaveBeenCalledWith( + expect.objectContaining({ + query: DevSessionDelete, + shopFqdn: 'test.myshopify.com', + variables: {clientId: 'client-id-123'}, + }), + ) + }) }) describe('deploy', () => { @@ -1140,10 +1222,10 @@ describe('deploy', () => { // Then expect(vi.mocked(appManagementRequestDoc)).toHaveBeenCalledWith({ - query: expect.anything(), + query: CreateAppVersion, token: 'token', variables: { - appId: 'gid://shopify/App/123', + clientId: 'api-key', version: { source: { name: 'Test App', @@ -1203,10 +1285,10 @@ describe('deploy', () => { // Then expect(vi.mocked(appManagementRequestDoc)).toHaveBeenCalledWith({ - query: expect.anything(), + query: CreateAppVersion, token: 'token', variables: { - appId: 'gid://shopify/App/123', + clientId: 'api-key', version: { sourceUrl: bundleUrl, }, @@ -1350,7 +1432,7 @@ describe('deploy', () => { expect(vi.mocked(appManagementRequestDoc)).toHaveBeenCalledWith({ query: AppVersions, token: 'token', - variables: expect.objectContaining({appId}), + variables: {clientId: 'api-key'}, unauthorizedHandler: { handler: expect.any(Function), type: 'token_refresh', @@ -1395,6 +1477,122 @@ describe('deploy', () => { }) }) +describe('client ID version and install requests', () => { + test('publishes a deployed version using the client ID while keeping the numeric dashboard link', async () => { + const client = AppManagementClient.getInstance() + client.token = () => Promise.resolve('token') + const versionId = 'gid://shopify/Version/456' + vi.mocked(appManagementRequestDoc) + .mockResolvedValueOnce({ + appVersionCreate: { + version: {id: versionId, metadata: {versionTag: '1.0.0', message: 'Test deploy'}, appModules: []}, + userErrors: [], + }, + }) + .mockResolvedValueOnce({appReleaseCreate: {release: null, userErrors: []}}) + + const result = await client.deploy({ + appManifest: {name: 'Test App', handle: 'test-app', modules: []}, + apiKey: 'client-id-123', + appId: 'gid://shopify/App/123', + name: 'Test App', + organizationId: 'gid://shopify/Organization/5', + }) + + expect(appManagementRequestDoc).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + query: CreateAppVersion, + variables: { + clientId: 'client-id-123', + version: {source: {name: 'Test App', handle: 'test-app', modules: []}}, + metadata: {versionTag: undefined, message: undefined, sourceControlUrl: undefined}, + }, + }), + ) + expect(appManagementRequestDoc).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({query: ReleaseVersion, variables: {clientId: 'client-id-123', versionId}}), + ) + expect(result.appDeploy.appVersion?.location).toBe('https://dev.shopify.com/dashboard/5/apps/123/versions/456') + expect(result.appDeploy.appVersion?.uuid).toBe(versionId) + }) + + test('releases an existing version using the client ID while keeping the numeric dashboard link', async () => { + const client = AppManagementClient.getInstance() + client.token = () => Promise.resolve('token') + vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({ + appReleaseCreate: { + release: {version: {id: 'gid://shopify/Version/456', metadata: {versionTag: '1.0.0', message: 'Release'}}}, + userErrors: [], + }, + }) + + const result = await client.release({ + app: {id: 'gid://shopify/App/123', apiKey: 'client-id-123', organizationId: '5', title: 'Test App'}, + version: {versionId: 'gid://shopify/Version/456', appVersionId: 456}, + }) + + expect(appManagementRequestDoc).toHaveBeenCalledWith( + expect.objectContaining({ + query: ReleaseVersion, + variables: {clientId: 'client-id-123', versionId: 'gid://shopify/Version/456'}, + }), + ) + expect(result.appRelease.appVersion).toEqual({ + versionTag: '1.0.0', + message: 'Release', + location: 'https://dev.shopify.com/dashboard/5/apps/123/versions/456', + }) + }) + + test('scopes the tag lookup to the client ID while retaining the version UUID and numeric link', async () => { + const client = AppManagementClient.getInstance() + client.token = () => Promise.resolve('token') + vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({ + versionByTag: { + id: 'gid://shopify/Version/456', + metadata: {versionTag: '1.0.0', message: 'Tagged version'}, + appModules: [], + }, + }) + + const result = await client.appVersionByTag( + {id: 'gid://shopify/App/123', apiKey: 'client-id-123', organizationId: '5', title: 'Test App'}, + '1.0.0', + ) + + expect(appManagementRequestDoc).toHaveBeenCalledWith( + expect.objectContaining({ + query: AppVersionByTag, + variables: {clientId: 'client-id-123', versionTag: '1.0.0'}, + }), + ) + expect(result.uuid).toBe('gid://shopify/Version/456') + expect(result.versionTag).toBe('1.0.0') + expect(result.message).toBe('Tagged version') + expect(result.location).toBe('https://dev.shopify.com/dashboard/5/apps/123/versions/456') + expect(result.appModuleVersions).toEqual([]) + }) + + test('reads the install count by client ID for delete warnings', async () => { + const client = AppManagementClient.getInstance() + client.token = () => Promise.resolve('token') + vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({app: {installCount: 17}}) + + const count = await client.appInstallCount({ + id: 'gid://shopify/App/123', + apiKey: 'client-id-123', + organizationId: '5', + }) + + expect(appManagementRequestDoc).toHaveBeenCalledWith( + expect.objectContaining({query: AppInstallCount, variables: {clientId: 'client-id-123'}}), + ) + expect(count).toBe(17) + }) +}) + describe('appVersions', () => { test('preserves a missing app in the API response instead of dereferencing it', async () => { // Given @@ -1459,6 +1657,50 @@ describe('AppManagementClient', () => { }) }) + test('separates Dev and deploy signed-upload cache keys for the same app and command run', async () => { + const client = AppManagementClient.getInstance() + client.token = () => Promise.resolve('token') + const appId = 'gid://shopify/App/123' + const clientId = 'client-id-123' + const organizationId = '5' + const mockResponse = { + appRequestSourceUploadUrl: {sourceUploadUrl: 'https://example.com/upload-url', userErrors: []}, + } + vi.mocked(appManagementRequestDoc) + .mockResolvedValueOnce(mockResponse) + .mockResolvedValueOnce(mockResponse) + .mockResolvedValueOnce(mockResponse) + + const devApp: MinimalAppIdentifiers = {apiKey: appId, id: appId, organizationId} + const deployApp: MinimalAppIdentifiers = {apiKey: clientId, id: clientId, organizationId} + await expect(getUploadURL(client, devApp)).resolves.toBe('https://example.com/upload-url') + await expect(getUploadURL(client, deployApp)).resolves.toBe('https://example.com/upload-url') + await expect(getUploadURL(client, devApp)).resolves.toBe('https://example.com/upload-url') + + expect(appManagementRequestDoc).toHaveBeenCalledTimes(3) + const requests = vi.mocked(appManagementRequestDoc).mock.calls.map(([request]) => request) + for (const request of requests) { + expect(request).toEqual( + expect.objectContaining({ + query: CreateAssetUrl, + variables: {sourceExtension: 'BR', organizationId: 'gid://shopify/Organization/5'}, + cacheOptions: {cacheTTL: {minutes: 59}, cacheExtraKey: expect.any(String)}, + }), + ) + } + + const cacheExtraKeys = requests.map( + (request) => (request as {cacheOptions?: {cacheExtraKey?: string}}).cacheOptions?.cacheExtraKey, + ) + const commandRunId = cacheExtraKeys[0]?.replace(`${appId}-`, '') + expect(commandRunId).toMatch(/^[\da-f-]{36}$/) + expect(cacheExtraKeys).toEqual([ + `${appId}-${commandRunId}`, + `${clientId}-${commandRunId}`, + `${appId}-${commandRunId}`, + ]) + }) + test('produces different cache keys for different apps in the same organization', async () => { // Given const client = AppManagementClient.getInstance() @@ -1499,7 +1741,7 @@ describe('AppManagementClient', () => { }) describe('generateSourceScanUploadUrl', () => { - test('passes the app ID and byte size, does not cache, and maps the upload response', async () => { + test('passes the client ID and byte size, does not cache, and maps the upload response', async () => { const client = AppManagementClient.getInstance() client.token = () => Promise.resolve('token') vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({ @@ -1510,7 +1752,7 @@ describe('AppManagementClient', () => { }) const result = await client.generateSourceScanUploadUrl({ - appId: 'gid://shopify/App/1', + clientId: 'client-id-1', byteSize: 1234, }) @@ -1519,7 +1761,7 @@ describe('AppManagementClient', () => { expect.objectContaining({ query: RequestSourceScanUploadUrl, token: 'token', - variables: {appId: 'gid://shopify/App/1', byteSize: 1234}, + variables: {clientId: 'client-id-1', byteSize: 1234}, }), ) expect(vi.mocked(appManagementRequestDoc).mock.calls[0]![0]).not.toHaveProperty('cacheOptions') @@ -1527,7 +1769,7 @@ describe('AppManagementClient', () => { }) describe('createSourceScan', () => { - test('passes the app ID and source scan URL and maps the accepted result', async () => { + test('passes the client ID and source scan URL and maps the accepted result', async () => { const client = AppManagementClient.getInstance() client.token = () => Promise.resolve('token') vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({ @@ -1535,7 +1777,7 @@ describe('AppManagementClient', () => { }) const result = await client.createSourceScan({ - appId: 'gid://shopify/App/1', + clientId: 'client-id-1', sourceScanUrl: 'https://example.com/source-scan-upload', }) @@ -1545,7 +1787,7 @@ describe('AppManagementClient', () => { query: CreateSourceScan, token: 'token', variables: { - appId: 'gid://shopify/App/1', + clientId: 'client-id-1', sourceScanUrl: 'https://example.com/source-scan-upload', }, }), 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 a944b75c567..4cfdb4eccb4 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 @@ -85,11 +85,11 @@ import { ProvisionShopAccessMutationVariables, } from '../../api/graphql/business-platform-organizations/generated/provision_shop_access.js' import {Store} from '../../api/graphql/business-platform-organizations/generated/types.js' +import {ReleasedAppModuleFragment} from '../../api/graphql/app-management/generated/active-app-release.js' import { - ActiveAppReleaseQuery, - ReleasedAppModuleFragment, -} from '../../api/graphql/app-management/generated/active-app-release.js' -import {ActiveAppReleaseFromApiKey} from '../../api/graphql/app-management/generated/active-app-release-from-api-key.js' + ActiveAppReleaseFromApiKey, + ActiveAppReleaseFromApiKeyQuery, +} from '../../api/graphql/app-management/generated/active-app-release-from-api-key.js' import {ReleaseVersion} from '../../api/graphql/app-management/generated/release-version.js' import { CreateAppVersion, @@ -619,9 +619,9 @@ export class AppManagementClient implements DeveloperPlatformClient { } } - async appVersions({id, organizationId, title}: MinimalOrganizationApp): Promise { + async appVersions({apiKey, organizationId, title}: MinimalOrganizationApp): Promise { const query = AppVersions - const variables = {appId: id} + const variables = {clientId: apiKey} const result = await this.appManagementRequest({query, variables}) if (!result.app) { return {app: null} @@ -654,19 +654,19 @@ export class AppManagementClient implements DeveloperPlatformClient { } } - async appInstallCount({id}: MinimalAppIdentifiers): Promise { + async appInstallCount({apiKey}: MinimalAppIdentifiers): Promise { const query = AppInstallCount - const variables = {appId: id} + const variables = {clientId: apiKey} const result = await this.appManagementRequest({query, variables}) return result.app.installCount ?? 0 } async appVersionByTag( - {id: appId, organizationId}: MinimalOrganizationApp, + {id: appId, apiKey, organizationId}: MinimalOrganizationApp, versionTag: string, ): Promise { const query = AppVersionByTag - const variables = {versionTag} + const variables = {clientId: apiKey, versionTag} const result = await this.appManagementRequest({query, variables}) const version = result.versionByTag if (!version) { @@ -749,8 +749,11 @@ export class AppManagementClient implements DeveloperPlatformClient { } } - async generateSourceScanUploadUrl({appId, byteSize}: SourceScanUploadUrlInput): Promise { - const variables: RequestSourceScanUploadUrlMutationVariables = {appId, byteSize} + async generateSourceScanUploadUrl({ + clientId, + byteSize, + }: SourceScanUploadUrlInput): Promise { + const variables: RequestSourceScanUploadUrlMutationVariables = {clientId, byteSize} const result = await this.appManagementRequest({ query: RequestSourceScanUploadUrl, variables, @@ -758,8 +761,8 @@ export class AppManagementClient implements DeveloperPlatformClient { return result.appRequestSourceScanUploadUrl } - async createSourceScan({appId, sourceScanUrl}: SourceScanCreateInput): Promise { - const variables: CreateSourceScanMutationVariables = {appId, sourceScanUrl} + async createSourceScan({clientId, sourceScanUrl}: SourceScanCreateInput): Promise { + const variables: CreateSourceScanMutationVariables = {clientId, sourceScanUrl} const result = await this.appManagementRequest({query: CreateSourceScan, variables}) return result.appSourceScanCreate } @@ -767,6 +770,7 @@ export class AppManagementClient implements DeveloperPlatformClient { async deploy({ appManifest, appId, + apiKey, organizationId, versionTag, message, @@ -779,7 +783,7 @@ export class AppManagementClient implements DeveloperPlatformClient { ? {sourceUrl: bundleUrl} : {source: appManifest} - const variables: CreateAppVersionMutationVariables = {appId, version: queryVersion, metadata} + const variables: CreateAppVersionMutationVariables = {clientId: apiKey, version: queryVersion, metadata} const result = await this.appManagementRequest({ query: CreateAppVersion, @@ -812,7 +816,7 @@ export class AppManagementClient implements DeveloperPlatformClient { } if (noRelease) return versionResult - const releaseVariables = {appId, versionId: version.id} + const releaseVariables = {clientId: apiKey, versionId: version.id} const releaseResult = await this.appManagementRequest({ query: ReleaseVersion, variables: releaseVariables, @@ -827,13 +831,13 @@ export class AppManagementClient implements DeveloperPlatformClient { } async release({ - app: {id: appId, organizationId}, + app: {id: appId, apiKey, organizationId}, version: {versionId}, }: { app: MinimalOrganizationApp version: AppVersionIdentifiers }): Promise { - const releaseVariables = {appId, versionId} + const releaseVariables = {clientId: apiKey, versionId} const releaseResult = await this.appManagementRequest({ query: ReleaseVersion, variables: releaseVariables, @@ -1026,18 +1030,17 @@ export class AppManagementClient implements DeveloperPlatformClient { } async devSessionCreate({ - appId, + clientId, assetsUrl, shopFqdn, websocketUrl, unsafeValidation, }: DevSessionCreateOptions): Promise { - const appIdNumber = String(numberFromGid(appId)) return this.appDevRequest({ query: DevSessionCreate, shopFqdn, variables: { - appId: appIdNumber, + clientId, assetsUrl: assetsUrl ?? '', websocketUrl, unsafeValidation: unsafeValidation ?? false, @@ -1047,16 +1050,15 @@ export class AppManagementClient implements DeveloperPlatformClient { } async devSessionUpdate({ - appId, + clientId, assetsUrl, shopFqdn, manifest, inheritedModuleUids, unsafeValidation, }: DevSessionUpdateOptions): Promise { - const appIdNumber = String(numberFromGid(appId)) const variables: DevSessionUpdateMutationVariables = { - appId: appIdNumber, + clientId, assetsUrl, manifest: JSON.stringify(manifest), inheritedModuleUids, @@ -1065,9 +1067,8 @@ export class AppManagementClient implements DeveloperPlatformClient { return this.appDevRequest({query: DevSessionUpdate, shopFqdn, variables}) } - async devSessionDelete({appId, shopFqdn}: DevSessionDeleteOptions): Promise { - const appIdNumber = String(numberFromGid(appId)) - return this.appDevRequest({query: DevSessionDelete, shopFqdn, variables: {appId: appIdNumber}}) + async devSessionDelete({clientId, shopFqdn}: DevSessionDeleteOptions): Promise { + return this.appDevRequest({query: DevSessionDelete, shopFqdn, variables: {clientId}}) } async getCreateDevStoreLink(org: Organization): Promise { @@ -1078,7 +1079,7 @@ export class AppManagementClient implements DeveloperPlatformClient { ] } - private async activeAppVersionRawResult(apiKey: string): Promise { + private async activeAppVersionRawResult(apiKey: string): Promise { return this.appManagementRequest({query: ActiveAppReleaseFromApiKey, variables: {apiKey}}) } From 6295c4580d57b9523bac5ae814832bc3d25b7577 Mon Sep 17 00:00:00 2001 From: Donald Merand Date: Wed, 30 Sep 2026 16:18:24 -0400 Subject: [PATCH 2/4] Pass only client IDs to security scan and install count requests --- .../app/src/cli/models/app/app.test-data.ts | 2 +- .../services/app-security-submit-api.test.ts | 12 ++--- .../cli/services/app-security-submit-api.ts | 7 ++- .../deploy-identifier-matching.test.ts | 44 +++++++++++++++++ .../context/deploy-identifier-matching.ts | 17 ++----- .../src/cli/services/security-submit.test.ts | 47 +++++++++++++++---- .../app/src/cli/services/security-submit.ts | 6 +-- .../utilities/developer-platform-client.ts | 2 +- .../app-management-client.test.ts | 6 +-- .../app-management-client.ts | 4 +- 10 files changed, 100 insertions(+), 47 deletions(-) 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 935e9ea5c17..569726a9b98 100644 --- a/packages/app/src/cli/models/app/app.test-data.ts +++ b/packages/app/src/cli/models/app/app.test-data.ts @@ -1337,7 +1337,7 @@ export function testDeveloperPlatformClient( orgFromId: (_organizationId: string) => Promise.resolve(testOrganization()), appsForOrg: (_organizationId: string) => Promise.resolve({apps: [testOrganizationApp()], hasMorePages: false}), specifications: (_app: MinimalAppIdentifiers) => Promise.resolve(testRemoteSpecifications), - appInstallCount: (_app: MinimalAppIdentifiers) => Promise.resolve(0), + appInstallCount: (_clientId: string) => Promise.resolve(0), templateSpecifications: (_app: MinimalAppIdentifiers) => Promise.resolve({templates: testRemoteExtensionTemplates, groupOrder: []}), orgAndApps: (_orgId: string) => diff --git a/packages/app/src/cli/services/app-security-submit-api.test.ts b/packages/app/src/cli/services/app-security-submit-api.test.ts index c863c833919..ecceabe9171 100644 --- a/packages/app/src/cli/services/app-security-submit-api.test.ts +++ b/packages/app/src/cli/services/app-security-submit-api.test.ts @@ -7,11 +7,7 @@ import type {AppSecuritySubmission} from './app-security-engine/index.js' import type {uploadToGCS} from './bundle.js' import type {SourceScanCreateSchema, SourceScanUploadUrlSchema} from '../utilities/developer-platform-client.js' -const app = { - apiKey: 'api-key', - organizationId: '123', - id: 'gid://shopify/App/1', -} +const clientId = 'api-key' const submission = {schemaVersion: 1, report: {}} as AppSecuritySubmission @@ -29,7 +25,7 @@ function options() { const createSourceScan = vi.fn(async (): Promise => ({accepted: true, userErrors: []})) return { input: { - app, + clientId, payload: {submission, bytes: Buffer.from(JSON.stringify(submission))}, // testDeveloperPlatformClient defaults are plain functions, not spies. // Always inject explicit vi.fn stubs before making call/mocking assertions. @@ -190,7 +186,7 @@ describe('submitAppSecurityScan', () => { await expect(submitAppSecurityScan(input, {upload})).resolves.toEqual({status: 'submitted'}) expect(generateSourceScanUploadUrl).toHaveBeenCalledWith({ - clientId: app.apiKey, + clientId, byteSize: input.payload.bytes.length, }) expect(generateSourceScanUploadUrl.mock.invocationCallOrder[0]).toBeLessThan(upload.mock.invocationCallOrder[0]!) @@ -200,7 +196,7 @@ describe('submitAppSecurityScan', () => { expect(bytes).toBe(input.payload.bytes) expect(uploadOptions).toEqual({artifactName: 'App Security submission', contentType: 'application/json'}) expect(createSourceScan).toHaveBeenCalledWith({ - clientId: app.apiKey, + clientId, sourceScanUrl: 'source-scan-upload-url', }) }) diff --git a/packages/app/src/cli/services/app-security-submit-api.ts b/packages/app/src/cli/services/app-security-submit-api.ts index 81a6b483e05..3a6707b5699 100644 --- a/packages/app/src/cli/services/app-security-submit-api.ts +++ b/packages/app/src/cli/services/app-security-submit-api.ts @@ -2,11 +2,10 @@ import {uploadToGCS} from './bundle.js' import {securitySubmitFailure} from './security-submit-result.js' import type {AppSecuritySubmissionPayload} from './app-security-submission-payload.js' import type {SecuritySubmitError, SubmitAppSecurityScanResult} from './security-submit-result.js' -import type {MinimalAppIdentifiers} from '../models/organization.js' import type {DeveloperPlatformClient} from '../utilities/developer-platform-client.js' export interface SubmitAppSecurityScanOptions { - app: MinimalAppIdentifiers + clientId: string payload: AppSecuritySubmissionPayload developerPlatformClient: DeveloperPlatformClient } @@ -28,7 +27,7 @@ export async function submitAppSecurityScan( let stage: SecuritySubmitError['stage'] = 'upload-url' try { const uploadResult = await options.developerPlatformClient.generateSourceScanUploadUrl({ - clientId: options.app.apiKey, + clientId: options.clientId, byteSize: options.payload.bytes.length, }) if (!uploadResult.sourceScanUploadUrl || uploadResult.userErrors.length > 0) { @@ -50,7 +49,7 @@ export async function submitAppSecurityScan( stage = 'create' const createResult = await options.developerPlatformClient.createSourceScan({ - clientId: options.app.apiKey, + clientId: options.clientId, sourceScanUrl: uploadResult.sourceScanUploadUrl, }) if (createResult.userErrors.length > 0 || !createResult.accepted) { diff --git a/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts b/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts index 64b951d285e..d11d143d67e 100644 --- a/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts +++ b/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts @@ -772,6 +772,50 @@ describe('ensureDeployIdentifiersFromAppVersion', () => { }) }) + test('passes the remote client ID and install count to the delete warning', async () => { + const appInstallCount = vi.fn().mockResolvedValue(17) + + await ensureDeployIdentifiersFromAppVersion( + deployOptions({ + activeAppVersion: {appModuleVersions: [REMOTE_EXTENSION_DELETED]}, + developerPlatformClient: testDeveloperPlatformClient({appInstallCount}), + }), + ) + + expect(appInstallCount).toHaveBeenCalledExactlyOnceWith(REMOTE_APP.apiKey) + expect(deployOrReleaseConfirmationPrompt).toHaveBeenCalledWith( + expect.objectContaining({ + extensionIdentifiersBreakdown: expect.objectContaining({ + onlyRemote: [{title: 'Deleted Extension', uid: 'deleted-uid', experience: 'extension'}], + }), + installCount: 17, + }), + ) + }) + + test('still prompts to delete when the install count request fails', async () => { + const appInstallCount = vi + .fn() + .mockRejectedValue(new Error('Unavailable')) + + await ensureDeployIdentifiersFromAppVersion( + deployOptions({ + activeAppVersion: {appModuleVersions: [REMOTE_EXTENSION_DELETED]}, + developerPlatformClient: testDeveloperPlatformClient({appInstallCount}), + }), + ) + + expect(appInstallCount).toHaveBeenCalledExactlyOnceWith(REMOTE_APP.apiKey) + expect(deployOrReleaseConfirmationPrompt).toHaveBeenCalledWith( + expect.objectContaining({ + extensionIdentifiersBreakdown: expect.objectContaining({ + onlyRemote: [{title: 'Deleted Extension', uid: 'deleted-uid', experience: 'extension'}], + }), + installCount: undefined, + }), + ) + }) + test('runs extension migrations before classifying the app version', async () => { const legacyRemoteExtension = { uuid: 'legacy-uuid-a', diff --git a/packages/app/src/cli/services/context/deploy-identifier-matching.ts b/packages/app/src/cli/services/context/deploy-identifier-matching.ts index 04236f1173d..71dba6d51b3 100644 --- a/packages/app/src/cli/services/context/deploy-identifier-matching.ts +++ b/packages/app/src/cli/services/context/deploy-identifier-matching.ts @@ -9,7 +9,6 @@ import {EnsureDeploymentIdsPresenceOptions} from './identifiers.js' import {remoteAppConfigurationExtensionContent} from '../app/select-app.js' import {AppInterface} from '../../models/app/app.js' import {DeployIdentifiers, ExtensionUuidsByLocalIdentifier} from '../../models/app/identifiers.js' -import {MinimalOrganizationApp} from '../../models/organization.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {deployOrReleaseConfirmationPrompt} from '../../prompts/deploy-release.js' import {AppModuleVersion, AppVersion} from '../../utilities/developer-platform-client.js' @@ -45,7 +44,9 @@ export async function ensureDeployIdentifiersFromAppVersion( const shouldFetchInstallCount = options.release && !options.allowDeletes && extensionIdentifiersBreakdown.onlyRemote.length > 0 - const installCount = shouldFetchInstallCount ? await fetchInstallCount(options).catch(() => undefined) : undefined + const installCount = shouldFetchInstallCount + ? await options.developerPlatformClient.appInstallCount(options.remoteApp.apiKey).catch(() => undefined) + : undefined const confirmed = await deployOrReleaseConfirmationPrompt({ extensionIdentifiersBreakdown, @@ -204,15 +205,3 @@ function buildRemoteBreakdownInfo(remote: AppModuleVersion) { } return buildExtensionBreakdownInfo(remote.registrationTitle, remote.registrationId) } - -/** Fetches install count for delete warnings. */ -async function fetchInstallCount(options: { - developerPlatformClient: EnsureDeploymentIdsPresenceOptions['developerPlatformClient'] - remoteApp: MinimalOrganizationApp -}) { - return options.developerPlatformClient.appInstallCount({ - id: options.remoteApp.id, - apiKey: options.remoteApp.apiKey, - organizationId: options.remoteApp.organizationId, - }) -} diff --git a/packages/app/src/cli/services/security-submit.test.ts b/packages/app/src/cli/services/security-submit.test.ts index b7536ba8085..379b8311667 100644 --- a/packages/app/src/cli/services/security-submit.test.ts +++ b/packages/app/src/cli/services/security-submit.test.ts @@ -40,12 +40,7 @@ function testDependencies(directory: string): SecuritySubmitDependencies { ), resolveClientId: vi.fn(async ({clientId}) => clientId ?? 'api-key'), fetchApp: vi.fn(async (clientId: string) => ({ - remoteApp: { - apiKey: clientId, - organizationId: '123', - id: 'gid://shopify/App/1', - title: 'Example app', - }, + remoteApp: {apiKey: clientId, title: 'Example app'}, developerPlatformClient: testDeveloperPlatformClient(), })), buildSubmission: vi.fn(buildSubmission), @@ -509,6 +504,42 @@ describe('securitySubmit', () => { }) }) + test('uses the fetched app key for both scan mutations and its title for confirmation and output', async () => { + await inTemporaryDirectory(async (directory) => { + const dependencies = testDependencies(directory) + const generateSourceScanUploadUrl = vi.fn(async () => ({ + sourceScanUploadUrl: 'source-scan-upload-url', + userErrors: [], + })) + const createSourceScan = vi.fn(async () => ({accepted: true, userErrors: []})) + const developerPlatformClient = testDeveloperPlatformClient({generateSourceScanUploadUrl, createSourceScan}) + vi.mocked(dependencies.fetchApp).mockResolvedValue({ + remoteApp: {apiKey: 'fetched-client-id', title: 'Fetched app title'}, + developerPlatformClient, + }) + vi.mocked(dependencies.canPrompt).mockReturnValue(true) + const upload = vi.fn().mockResolvedValue(undefined) + vi.mocked(dependencies.submitScan).mockImplementation((input) => submitAppSecurityScan(input, {upload})) + + const result = await securitySubmit({...options(directory), clientId: 'selected-client-id'}, dependencies) + + expect(dependencies.fetchApp).toHaveBeenCalledExactlyOnceWith('selected-client-id') + expect(dependencies.confirm).toHaveBeenCalledWith(expect.objectContaining({appTitle: 'Fetched app title'})) + expect(dependencies.submitScan).toHaveBeenCalledWith( + expect.objectContaining({clientId: 'fetched-client-id', developerPlatformClient}), + ) + expect(generateSourceScanUploadUrl).toHaveBeenCalledExactlyOnceWith({ + clientId: 'fetched-client-id', + byteSize: vi.mocked(dependencies.submitScan).mock.calls[0]![0].payload.bytes.length, + }) + expect(createSourceScan).toHaveBeenCalledExactlyOnceWith({ + clientId: 'fetched-client-id', + sourceScanUrl: 'source-scan-upload-url', + }) + expect(result).toMatchObject({status: 'submitted', clientId: 'fetched-client-id', appTitle: 'Fetched app title'}) + }) + }) + describe.each([false, true])('target resolution (json=%s)', (json) => { test.each([ { @@ -550,9 +581,7 @@ describe('securitySubmit', () => { expect(dependencies.resolveClientId).toHaveBeenCalledExactlyOnceWith({directory, clientId, configName}) expect(dependencies.fetchApp).toHaveBeenCalledExactlyOnceWith(expectedClientId) - expect(dependencies.submitScan).toHaveBeenCalledWith( - expect.objectContaining({app: expect.objectContaining({apiKey: expectedClientId})}), - ) + expect(dependencies.submitScan).toHaveBeenCalledWith(expect.objectContaining({clientId: expectedClientId})) await expect(fileExists(joinPath(directory, '.shopify', 'project.json'))).resolves.toBe(false) }) }, diff --git a/packages/app/src/cli/services/security-submit.ts b/packages/app/src/cli/services/security-submit.ts index 8874c3b5b17..c676833ab47 100644 --- a/packages/app/src/cli/services/security-submit.ts +++ b/packages/app/src/cli/services/security-submit.ts @@ -18,7 +18,6 @@ import type {ReadTraceResult, ResolvedAppSecurityArtifactPaths} from './app-secu import type {SubmitAppSecurityScanOptions} from './app-security-submit-api.js' import type {SecuritySubmitConfirmationAction, SecuritySubmitConfirmationInput} from './security-submit-output.js' import type {SecuritySubmitResult, SubmitAppSecurityScanResult} from './security-submit-result.js' -import type {MinimalAppIdentifiers} from '../models/organization.js' import type {DeveloperPlatformClient} from '../utilities/developer-platform-client.js' export interface SecuritySubmitOptions { @@ -32,7 +31,8 @@ export interface SecuritySubmitOptions { feedback?: string } -interface SecuritySubmitApp extends MinimalAppIdentifiers { +interface SecuritySubmitApp { + apiKey: string title: string } @@ -173,7 +173,7 @@ export default async function securitySubmit( } const result = await dependencies.submitScan({ - app: remoteApp, + clientId: remoteApp.apiKey, payload, developerPlatformClient, }) diff --git a/packages/app/src/cli/utilities/developer-platform-client.ts b/packages/app/src/cli/utilities/developer-platform-client.ts index a903280e8ac..f442c54d235 100644 --- a/packages/app/src/cli/utilities/developer-platform-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client.ts @@ -237,7 +237,7 @@ export interface DeveloperPlatformClient { activeAppVersion?: AppVersion, ) => Promise appVersions: (app: OrganizationApp) => Promise - appInstallCount: (app: MinimalAppIdentifiers) => Promise + appInstallCount: (clientId: string) => Promise activeAppVersion: (app: MinimalAppIdentifiers) => Promise appVersionByTag: (app: MinimalOrganizationApp, tag: string) => Promise appVersionsDiff: (app: MinimalOrganizationApp, version: AppVersionIdentifiers) => Promise diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts index e3562e283a7..fb0b6ebbdf7 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts @@ -1580,11 +1580,7 @@ describe('client ID version and install requests', () => { client.token = () => Promise.resolve('token') vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({app: {installCount: 17}}) - const count = await client.appInstallCount({ - id: 'gid://shopify/App/123', - apiKey: 'client-id-123', - organizationId: '5', - }) + const count = await client.appInstallCount('client-id-123') expect(appManagementRequestDoc).toHaveBeenCalledWith( expect.objectContaining({query: AppInstallCount, variables: {clientId: 'client-id-123'}}), 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 4cfdb4eccb4..ef760b588ba 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 @@ -654,9 +654,9 @@ export class AppManagementClient implements DeveloperPlatformClient { } } - async appInstallCount({apiKey}: MinimalAppIdentifiers): Promise { + async appInstallCount(clientId: string): Promise { const query = AppInstallCount - const variables = {clientId: apiKey} + const variables = {clientId} const result = await this.appManagementRequest({query, variables}) return result.app.installCount ?? 0 } From 18206babbdcbde1b602dcd41570bf8fac023bf5e Mon Sep 17 00:00:00 2001 From: Donald Merand Date: Thu, 1 Oct 2026 10:07:23 -0400 Subject: [PATCH 3/4] Add changeset for app-scoped release version lookup --- .changeset/release-version-tag-app-scope.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/release-version-tag-app-scope.md diff --git a/.changeset/release-version-tag-app-scope.md b/.changeset/release-version-tag-app-scope.md new file mode 100644 index 00000000000..ed6d4af04ca --- /dev/null +++ b/.changeset/release-version-tag-app-scope.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Fix `app release --version` finding a version with the same tag from a different app From 080a35808577f2385afd3c3e3121a5c17e67d7e1 Mon Sep 17 00:00:00 2001 From: Donald Merand Date: Thu, 1 Oct 2026 10:51:47 -0400 Subject: [PATCH 4/4] Test scoped version-tag misses and the dev client ID --- packages/app/src/cli/services/dev.test.ts | 34 +++++++++++++++++++ .../app-management-client.test.ts | 22 +++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/app/src/cli/services/dev.test.ts b/packages/app/src/cli/services/dev.test.ts index 119576f884d..1411bff2afc 100644 --- a/packages/app/src/cli/services/dev.test.ts +++ b/packages/app/src/cli/services/dev.test.ts @@ -84,6 +84,40 @@ describe('dev', () => { addPublicMetadata.mockRestore() addSensitiveMetadata.mockRestore() }) + + test('passes the remote app client ID to the Dev UI', async () => { + const app = testAppLinked() + const developerPlatformClient = testDeveloperPlatformClient() + vi.mocked(fetchAppRemoteConfiguration).mockResolvedValue({name: 'Remote app', application_url: '', embedded: true}) + vi.mocked(getAvailableTCPPort).mockResolvedValue(3456) + vi.mocked(checkPortAvailability).mockResolvedValue(true) + vi.mocked(setupDevProcesses).mockResolvedValue({ + processes: [], + previewUrl: 'https://dev-store.myshopify.com/admin/apps/client-id-123', + graphiqlUrl: undefined, + devSessionStatusManager: {} as any, + }) + vi.mocked(renderDev).mockResolvedValue(undefined) + + await dev({ + app, + project: testProject({usesWorkspaces: true}), + remoteApp: testOrganizationApp({id: 'gid://shopify/App/123', apiKey: 'client-id-123'}), + organization: testOrganization(), + specifications: [], + developerPlatformClient, + store: testOrganizationStore({shopDomain: 'dev-store.myshopify.com'}), + directory: app.directory, + update: false, + commandConfig: {} as any, + skipDependenciesInstallation: true, + tunnel: {mode: 'custom', url: 'https://localhost:3456'}, + }) + + expect(renderDev).toHaveBeenCalledWith( + expect.objectContaining({app: {apiKey: 'client-id-123', developerPlatformClient}}), + ) + }) }) describe('blockIfMigrationIncomplete', () => { diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts index fb0b6ebbdf7..9621d742783 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts @@ -66,7 +66,7 @@ import { } from '@shopify/cli-kit/node/api/business-platform' import {appManagementRequestDoc} from '@shopify/cli-kit/node/api/app-management' import {appDevRequestDoc} from '@shopify/cli-kit/node/api/app-dev' -import {BugError} from '@shopify/cli-kit/node/error' +import {AbortError, BugError} from '@shopify/cli-kit/node/error' import {randomUUID} from '@shopify/cli-kit/node/crypto' import {webhooksRequestDoc} from '@shopify/cli-kit/node/api/webhooks' @@ -1575,6 +1575,26 @@ describe('client ID version and install requests', () => { expect(result.appModuleVersions).toEqual([]) }) + test('rejects a missing tag scoped to the app client ID', async () => { + const client = AppManagementClient.getInstance() + client.token = () => Promise.resolve('token') + vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({versionByTag: null}) + + const lookup = client.appVersionByTag( + {id: 'gid://shopify/App/123', apiKey: 'client-id-123', organizationId: '5', title: 'Test App'}, + '1.0.0', + ) + + await expect(lookup).rejects.toThrow(AbortError) + await expect(lookup).rejects.toThrow('Version not found for tag: 1.0.0') + expect(appManagementRequestDoc).toHaveBeenCalledWith( + expect.objectContaining({ + query: AppVersionByTag, + variables: {clientId: 'client-id-123', versionTag: '1.0.0'}, + }), + ) + }) + test('reads the install count by client ID for delete warnings', async () => { const client = AppManagementClient.getInstance() client.token = () => Promise.resolve('token')