11import { validateUrl } from '../../app/validation/common.js'
2- import { TransformationConfig , createConfigExtensionSpecification } from '../specification.js'
2+ import {
3+ CustomTransformationConfig ,
4+ TransformationConfig ,
5+ appConfigTransform ,
6+ createConfigExtensionSpecification ,
7+ } from '../specification.js'
38import { BaseSchemaWithoutHandle } from '../schemas.js'
49import { normalizeDelimitedString } from '@shopify/cli-kit/common/string'
510import { zod } from '@shopify/cli-kit/node/schema'
@@ -35,7 +40,7 @@ const AppAccessSchema = BaseSchemaWithoutHandle.extend({
3540
3641export const AppAccessSpecIdentifier = 'app_access'
3742
38- const AppAccessTransformConfig : TransformationConfig = {
43+ const AppAccessFieldMapping : TransformationConfig = {
3944 access : 'access' ,
4045 scopes : 'access_scopes.scopes' ,
4146 required_scopes : 'access_scopes.required_scopes' ,
@@ -44,6 +49,29 @@ const AppAccessTransformConfig: TransformationConfig = {
4449 redirect_url_allowlist : 'auth.redirect_urls' ,
4550}
4651
52+ /**
53+ * The platform stores Shop scopes (e.g. `shop_app:oauth`) in a separate `shop_scopes` field, split out of
54+ * `scopes` when a version is created. Locally they live in `access_scopes.scopes`, so fold them back in.
55+ * Without this, `config link` writes a TOML without the Shop scopes and the next deploy removes them.
56+ *
57+ * @param remoteContent - The app_access module config as returned by the platform
58+ * @returns The config with `shop_scopes` merged into `scopes` and removed
59+ */
60+ export function mergeShopScopesIntoScopes ( remoteContent : { [ key : string ] : unknown } ) : { [ key : string ] : unknown } {
61+ const { shop_scopes : shopScopes , ...rest } = remoteContent
62+ const shopScopesString = typeof shopScopes === 'string' ? normalizeDelimitedString ( shopScopes ) : undefined
63+ if ( ! shopScopesString ) return rest
64+
65+ const platformScopes = typeof rest . scopes === 'string' ? rest . scopes : ''
66+ return { ...rest , scopes : normalizeDelimitedString ( [ platformScopes , shopScopesString ] . join ( ',' ) ) }
67+ }
68+
69+ const AppAccessTransformConfig : CustomTransformationConfig = {
70+ forward : ( content ) => appConfigTransform ( content , AppAccessFieldMapping ) ,
71+ reverse : ( content ) =>
72+ appConfigTransform ( mergeShopScopesIntoScopes ( content as { [ key : string ] : unknown } ) , AppAccessFieldMapping , true ) ,
73+ }
74+
4775const appAccessSpec = createConfigExtensionSpecification ( {
4876 identifier : AppAccessSpecIdentifier ,
4977 schema : AppAccessSchema ,
0 commit comments