Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/app/src/cli/models/app/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,7 @@ describe('manifest', () => {
assets: appAccessModule.uid,
target: appAccessModule.contextValue,
config: expect.objectContaining({
redirect_url_allowlist: ['https://example.com/auth/callback'],
auth: {redirect_urls: ['https://example.com/auth/callback']},
}),
},
],
Expand Down Expand Up @@ -906,7 +906,7 @@ describe('manifest', () => {
assets: appAccess.uid,
target: appAccess.contextValue,
config: expect.objectContaining({
redirect_url_allowlist: ['https://new-url.io/auth/callback'],
auth: {redirect_urls: ['https://new-url.io/auth/callback']},
}),
},
],
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/cli/models/extensions/specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type ExtensionFeature =
| 'localization'
| 'generates_source_maps'

export interface TransformationConfig {
interface TransformationConfig {
[key: string]: string
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,10 @@
import spec from './app_config_app_access.js'
import {placeholderAppConfiguration} from '../../app/app.test-data.js'
import {describe, expect, test} from 'vitest'

describe('app_config_app_access', () => {
describe('transform', () => {
test('should return the transformed object', () => {
// Given
const object = {
access: {
admin: {direct_api_mode: 'online'},
},
access_scopes: {
scopes: 'read_products,write_products',
optional_scopes: ['read_customers'],
required_scopes: ['write_orders', 'read_inventory'],
use_legacy_install_flow: true,
},
auth: {
redirect_urls: ['https://example.com/auth/callback'],
},
}
const appAccessSpec = spec

// When
const result = appAccessSpec.transformLocalToRemote!(object, placeholderAppConfiguration)

// Then
expect(result).toMatchObject({
access: {
admin: {direct_api_mode: 'online'},
},
scopes: 'read_products,write_products',
optional_scopes: ['read_customers'],
required_scopes: ['write_orders', 'read_inventory'],
use_legacy_install_flow: true,
redirect_url_allowlist: ['https://example.com/auth/callback'],
})
test('transformLocalToRemote should be undefined', () => {
expect(spec.transformLocalToRemote).toBeUndefined()
})
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {validateUrl} from '../../app/validation/common.js'
import {TransformationConfig, createConfigExtensionSpecification} from '../specification.js'
import {configWithoutFirstClassFields, createConfigExtensionSpecification} from '../specification.js'
import {BaseSchemaWithoutHandle} from '../schemas.js'
import {normalizeDelimitedString} from '@shopify/cli-kit/common/string'
import {zod} from '@shopify/cli-kit/node/schema'
Expand Down Expand Up @@ -33,19 +33,37 @@ const AppAccessSchema = BaseSchemaWithoutHandle.extend({

export const AppAccessSpecIdentifier = 'app_access'

const AppAccessTransformConfig: TransformationConfig = {
access: 'access',
scopes: 'access_scopes.scopes',
required_scopes: 'access_scopes.required_scopes',
optional_scopes: 'access_scopes.optional_scopes',
use_legacy_install_flow: 'access_scopes.use_legacy_install_flow',
redirect_url_allowlist: 'auth.redirect_urls',
}

const appAccessSpec = createConfigExtensionSpecification({
identifier: AppAccessSpecIdentifier,
schema: AppAccessSchema,
transformConfig: AppAccessTransformConfig,
deployConfig: async (config) => {
const {name, ...rest} = configWithoutFirstClassFields(config)
return rest
},
transformRemoteToLocal: (remoteContent: object) => {
const remote = remoteContent as {[key: string]: unknown}
const result: {[key: string]: unknown} = {}

if (remote.access !== undefined) {
result.access = remote.access
}

const accessScopes: {[key: string]: unknown} = {}
if (remote.scopes !== undefined) accessScopes.scopes = remote.scopes
if (remote.required_scopes !== undefined) accessScopes.required_scopes = remote.required_scopes
if (remote.optional_scopes !== undefined) accessScopes.optional_scopes = remote.optional_scopes
if (remote.use_legacy_install_flow !== undefined)
accessScopes.use_legacy_install_flow = remote.use_legacy_install_flow
if (Object.keys(accessScopes).length > 0) {
result.access_scopes = accessScopes
}

if (remote.redirect_url_allowlist !== undefined) {
result.auth = {redirect_urls: remote.redirect_url_allowlist}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auth is required in schema but can be omitted by transformRemoteToLocal

AppAccessSchema defines auth as a required object (no .optional()), but transformRemoteToLocal only sets result.auth when remote.redirect_url_allowlist !== undefined. If the platform returns remote content without redirect_url_allowlist (or returns null), the transform returns an object missing auth, which can fail schema validation/parsing downstream and potentially break CLI flows that ingest remote module content.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transformRemoteToLocal only recognizes redirect_url_allowlist and will drop nested auth.redirect_urls

Reverse transform currently only maps remote.redirect_url_allowlist into result.auth.redirect_urls. If the remote payload sends nested auth.redirect_urls (matching the new deployConfig output), this code will omit auth entirely in the local output, causing silent data loss during remote→local operations and potentially breaking auth callback flows.


return result

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transformRemoteToLocal forwards remote values without normalizing/validating nested types

transformRemoteToLocal casts remoteContent to {[key: string]: unknown} and forwards values directly (e.g., remote.redirect_url_allowlist -> redirect_urls). If remote values are unexpected (null, wrong type like string instead of string[]), the local config can become structurally incorrect and later fail validation or cause runtime errors depending on when parsing occurs.

},
getDevSessionUpdateMessages: async (config) => {
const hasAccessModule = config.access_scopes !== undefined
const isLegacyInstallFlow = config.access_scopes?.use_legacy_install_flow === true
Expand Down
Loading