Skip to content

Commit

Permalink
Migrate scopes to required_scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
zzooeeyy committed Feb 12, 2025
1 parent f96157d commit 54472ee
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 8 deletions.
25 changes: 23 additions & 2 deletions packages/app/src/cli/models/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {configurationFileNames} from '../../constants.js'
import {ApplicationURLs} from '../../services/dev/urls.js'
import appHomeSpec from '../extensions/specifications/app_config_app_home.js'
import appProxySpec from '../extensions/specifications/app_config_app_proxy.js'
import {replaceScopesWithRequiredScopesInToml} from '../../services/app/patch-app-configuration-file.js'
import {ZodObjectOf, zod} from '@shopify/cli-kit/node/schema'
import {DotEnvFile} from '@shopify/cli-kit/node/dot-env'
import {getDependencies, PackageManager, readAndParsePackageJson} from '@shopify/cli-kit/node/node-package-manager'
Expand Down Expand Up @@ -280,6 +281,7 @@ export interface AppInterface<
extensionsForType: (spec: {identifier: string; externalIdentifier: string}) => ExtensionInstance[]
updateExtensionUUIDS: (uuids: {[key: string]: string}) => void
preDeployValidation: () => Promise<void>
migratePendingSchemaChanges: () => Promise<void>
/**
* Checks if the app has any elements that means it can be "launched" -- can host its own app home section.
*
Expand Down Expand Up @@ -317,8 +319,7 @@ type AppConstructor<
export class App<
TConfig extends AppConfiguration = AppConfiguration,
TModuleSpec extends ExtensionSpecification = ExtensionSpecification,
> implements AppInterface<TConfig, TModuleSpec>
{
> implements AppInterface<TConfig, TModuleSpec> {

Check failure on line 322 in packages/app/src/cli/models/app/app.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/models/app/app.ts#L322

[prettier/prettier] Replace `·` with `⏎`
name: string
idEnvironmentVariableName: 'SHOPIFY_API_KEY' = 'SHOPIFY_API_KEY' as const
directory: string
Expand Down Expand Up @@ -422,6 +423,26 @@ export class App<
await writeFile(appHiddenConfigPath(this.directory), JSON.stringify(this.hiddenConfig, null, 2))
}

async migratePendingSchemaChanges() {
await this.migrateScopesToRequiredScopes()
await Promise.all([this.realExtensions.map((ext) => ext.migratePendingSchemaChanges())])
}

async migrateScopesToRequiredScopes() {
if (isCurrentAppSchema(this.configuration) && this.configuration.access_scopes?.scopes) {
const accessConfig = this.configuration as {
access_scopes: {scopes?: string; required_scopes?: string[]}
}
accessConfig.access_scopes = {
...accessConfig.access_scopes,
required_scopes: accessConfig.access_scopes.scopes?.split(',').map((scope) => scope.trim()),
}
delete accessConfig.access_scopes.scopes

await replaceScopesWithRequiredScopesInToml(this.configuration.path)
}
}

async preDeployValidation() {
const functionExtensionsWithUiHandle = this.allExtensions.filter(
(ext) => ext.isFunctionExtension && (ext.configuration as unknown as FunctionConfigType).ui?.handle,
Expand Down
5 changes: 5 additions & 0 deletions packages/app/src/cli/models/extensions/extension-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
return this.specification.preDeployValidation(this)
}

migratePendingSchemaChanges(): Promise<void> {
if (!this.specification.migratePendingSchemaChanges) return Promise.resolve()
return this.specification.migratePendingSchemaChanges(this)
}

buildValidation(): Promise<void> {
if (!this.specification.buildValidation) return Promise.resolve()
return this.specification.buildValidation(this)
Expand Down
5 changes: 5 additions & 0 deletions packages/app/src/cli/models/extensions/specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface ExtensionSpecification<TConfiguration extends BaseConfigType =
buildValidation?: (extension: ExtensionInstance<TConfiguration>) => Promise<void>
hasExtensionPointTarget?(config: TConfiguration, target: string): boolean
appModuleFeatures: (config?: TConfiguration) => ExtensionFeature[]
migratePendingSchemaChanges?: (extension: ExtensionInstance<TConfiguration>) => Promise<void>
getDevSessionActionUpdateMessage?: (
config: TConfiguration,
appConfig: CurrentAppConfiguration,
Expand Down Expand Up @@ -233,6 +234,8 @@ export function createConfigExtensionSpecification<TConfiguration extends BaseCo
appConfig: CurrentAppConfiguration,
storeFqdn: string,
) => Promise<string>
preDeployValidation?: (extension: ExtensionInstance<TConfiguration>) => Promise<void>
migratePendingSchemaChanges?: (extension: ExtensionInstance<TConfiguration>) => Promise<void>
}): ExtensionSpecification<TConfiguration> {
const appModuleFeatures = spec.appModuleFeatures ?? (() => [])
return createExtensionSpecification({
Expand All @@ -246,6 +249,8 @@ export function createConfigExtensionSpecification<TConfiguration extends BaseCo
experience: 'configuration',
uidStrategy: spec.uidStrategy ?? 'single',
getDevSessionActionUpdateMessage: spec.getDevSessionActionUpdateMessage,
preDeployValidation: spec.preDeployValidation,
migratePendingSchemaChanges: spec.migratePendingSchemaChanges,
})
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {buildAppURLForWeb} from '../../../utilities/app/app-url.js'
import {validateUrl} from '../../app/validation/common.js'
import {TransformationConfig, createConfigExtensionSpecification} from '../specification.js'
import {ExtensionInstance} from '../extension-instance.js'
import {outputContent, outputToken} from '@shopify/cli-kit/node/output'
import {normalizeDelimitedString} from '@shopify/cli-kit/common/string'
import {zod} from '@shopify/cli-kit/node/schema'
import {AbortError} from '@shopify/cli-kit/node/error'

const AppAccessSchema = zod.object({
access: zod
Expand Down Expand Up @@ -51,6 +53,34 @@ const appAccessSpec = createConfigExtensionSpecification({
const scopesURL = await buildAppURLForWeb(storeFqdn, appConfig.client_id)
return outputContent`Scopes updated. ${outputToken.link('Open app to accept scopes.', scopesURL)}`.value
},
preDeployValidation: async (extension) => {
return rejectScopes(extension)
},
migratePendingSchemaChanges: async (extension) => {
return migrateScopesToRequiredScopes(extension)
},
})

async function rejectScopes(extension: ExtensionInstance) {
const accessConfig = extension.configuration as {
access_scopes?: {scopes?: string}
}
if (accessConfig.access_scopes?.scopes) {
throw new AbortError('`scopes` are no longer supported. Use `required_scopes` instead.')
}
}

async function migrateScopesToRequiredScopes(extension: ExtensionInstance) {
const accessConfig = extension.configuration as {
access_scopes?: {scopes?: string; required_scopes?: string[]}
}

if (accessConfig.access_scopes?.scopes) {
accessConfig.access_scopes.required_scopes = accessConfig.access_scopes.scopes
.split(',')
.map((scope) => scope.trim())
delete accessConfig.access_scopes.scopes
}
}

export default appAccessSpec
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface AppConfigurationUsedByCli {
webhooks?: WebhooksConfig
access_scopes?: {
scopes?: string
required_scopes?: string[]
use_legacy_install_flow?: boolean
}
auth?: {
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/cli/prompts/deprecation-warnings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import metadata from '../metadata.js'
import {renderWarning} from '@shopify/cli-kit/node/ui'
import {renderConfirmationPrompt, renderWarning} from '@shopify/cli-kit/node/ui'

Check failure on line 2 in packages/app/src/cli/prompts/deprecation-warnings.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/prompts/deprecation-warnings.ts#L2

[unused-imports/no-unused-imports] 'renderConfirmationPrompt' is defined but never used.

Check failure on line 2 in packages/app/src/cli/prompts/deprecation-warnings.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/prompts/deprecation-warnings.ts#L2

[@typescript-eslint/no-unused-vars] 'renderConfirmationPrompt' is defined but never used. Allowed unused vars must match /^_/u.

export async function showApiKeyDeprecationWarning() {
await metadata.addPublicMetadata(() => ({
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/cli/services/app-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export async function linkedAppContext({
await addUidToTomlsIfNecessary(localApp.allExtensions, developerPlatformClient)
}

await localApp.migratePendingSchemaChanges()

return {app: localApp, remoteApp, developerPlatformClient, specifications, organization}
}

Expand Down
27 changes: 22 additions & 5 deletions packages/app/src/cli/services/app/config/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,13 +378,29 @@ async function overwriteLocalConfigFileWithRemoteAppConfiguration(options: {
remoteAppConfiguration = buildAppConfigurationFromRemoteAppProperties(remoteApp, locallyProvidedScopes)
}

// Create a clean version of the local config without scopes/require_scopes
const cleanLocalConfig = {...(localAppOptions.existingConfig ?? {})}

if ('access_scopes' in cleanLocalConfig) {
const accessScopes = cleanLocalConfig.access_scopes as {
scopes?: string
required_scopes?: string[]
}
// If remote has required_scopes, remove scopes from local
if (remoteAppConfiguration?.access_scopes?.required_scopes) {
delete accessScopes.scopes
}
// If remote has scopes, remove required_scopes from local
if (remoteAppConfiguration?.access_scopes?.scopes) {
delete accessScopes.required_scopes
}
}

const replaceLocalArrayStrategy = (_destinationArray: unknown[], sourceArray: unknown[]) => sourceArray

const mergedAppConfiguration = {
...deepMergeObjects<AppConfiguration, CurrentAppConfiguration>(
{
...(localAppOptions.existingConfig ?? {}),
},
cleanLocalConfig,
{
client_id: remoteApp.apiKey,
path: configFilePath,
Expand Down Expand Up @@ -536,7 +552,7 @@ function addRemoteAppAccessConfig(locallyProvidedScopes: string, remoteApp: Orga
// if we have upstream scopes, use them
if (remoteApp.requestedAccessScopes) {
accessScopesContent = {
scopes: remoteApp.requestedAccessScopes.join(','),
required_scopes: remoteApp.requestedAccessScopes,
}
// if we can't find scopes or have to fall back, omit setting a scope and set legacy to true
} else if (locallyProvidedScopes === '') {
Expand All @@ -546,10 +562,11 @@ function addRemoteAppAccessConfig(locallyProvidedScopes: string, remoteApp: Orga
// if we have scopes locally and not upstream, preserve them but don't push them upstream (legacy is true)
} else {
accessScopesContent = {
scopes: locallyProvidedScopes,
required_scopes: locallyProvidedScopes.split(',').map((scope) => scope.trim()),
use_legacy_install_flow: true,
}
}

return {
auth: {
redirect_urls: remoteApp.redirectUrlWhitelist ?? [],
Expand Down
21 changes: 21 additions & 0 deletions packages/app/src/cli/services/app/patch-app-configuration-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,24 @@ export async function patchAppConfigurationFile({path, patch, schema}: PatchToml
export function replaceArrayStrategy(_: unknown[], newArray: unknown[]): unknown[] {
return newArray
}

export async function replaceScopesWithRequiredScopesInToml(path: string) {
const tomlContents = await readFile(path)
const configuration = decodeToml(tomlContents)
if (
configuration.access_scopes &&
typeof configuration.access_scopes === 'object' &&
'scopes' in configuration.access_scopes &&
configuration.access_scopes.scopes
) {
const scopes = configuration.access_scopes.scopes
configuration.access_scopes = {
...configuration.access_scopes,
required_scopes: typeof scopes === 'string' ? scopes.split(',').map((scope) => scope.trim()) : [],
}
delete configuration.access_scopes.scopes
}
let encodedString = encodeToml(configuration)
encodedString = addDefaultCommentsToToml(encodedString)
await writeFile(path, encodedString)
}

0 comments on commit 54472ee

Please sign in to comment.