diff --git a/api/src/utils/market-app.utils.ts b/api/src/utils/market-app.utils.ts index 283e28e6..f0b486d0 100644 --- a/api/src/utils/market-app.utils.ts +++ b/api/src/utils/market-app.utils.ts @@ -2,11 +2,23 @@ import {client} from '@contentstack/marketplace-sdk'; import { DEVURLS } from '../constants/index.js'; - +const buildMarketplaceClient = ({ + authtoken, + region, +}: { + authtoken?: string; + region?: string; +}) => { + const host = DEVURLS?.[region as keyof typeof DEVURLS] ?? DEVURLS?.NA; + if (typeof authtoken === 'string' && authtoken.startsWith('Bearer ')) { + return client({ authorization: authtoken, host } as any); + } + return client({ authtoken, host } as any); +}; export const getAllApps = async ({ organizationUid, authtoken, region }: any) => { try { - const contentstackclient = client({ authtoken, host: DEVURLS?.[region] ?? DEVURLS?.NA }); + const contentstackclient = buildMarketplaceClient({ authtoken, region }); const data = await contentstackclient.marketplace(organizationUid).findAllApps(); return data?.items; } catch (err) { @@ -16,7 +28,7 @@ export const getAllApps = async ({ organizationUid, authtoken, region }: any) => export const getAppManifestAndAppConfig = async ({ organizationUid, authtoken, region, manifestUid }: any) => { try { - const contentstackclient = client({ authtoken, host: DEVURLS?.[region] ?? DEVURLS?.NA }); + const contentstackclient = buildMarketplaceClient({ authtoken, region }); const data = await contentstackclient.marketplace(organizationUid).app(manifestUid).fetch(); return data; } catch (err: any) { @@ -56,13 +68,8 @@ export const fetchMarketplaceInstallationsForStack = async ({ authtoken: string; region: string; }) => { - const host = DEVURLS?.[region as keyof typeof DEVURLS] ?? DEVURLS.NA; - try { - const contentstackclient = client({ - authtoken, - host, - }); + const contentstackclient = buildMarketplaceClient({ authtoken, region }); const instApi = contentstackclient .marketplace(organizationUid) .installation(); diff --git a/api/tests/unit/utils/market-app.utils.test.ts b/api/tests/unit/utils/market-app.utils.test.ts index ac5b2ff4..d34a9161 100644 --- a/api/tests/unit/utils/market-app.utils.test.ts +++ b/api/tests/unit/utils/market-app.utils.test.ts @@ -74,6 +74,31 @@ describe('market-app.utils', () => { }); }); + it('routes SSO Bearer tokens to the SDK `authorization` option (not `authtoken`)', async () => { + // SSO callers historically forward a `Bearer ` string in + // the `authtoken` arg. Routing that into the SDK's `authtoken` option + // puts a Bearer value into the wrong HTTP header and Developer Hub + // rejects it, which is what caused marketplace-app custom fields to + // silently disappear from SSO migrations. + mockClient.marketplace.mockReturnValue({ + findAllApps: vi.fn().mockResolvedValue({ items: [] }), + }); + + await getAllApps({ + organizationUid: 'org-123', + authtoken: 'Bearer sso-access-token', + region: 'NA', + }); + + expect(marketplaceClient).toHaveBeenCalledWith({ + authorization: 'Bearer sso-access-token', + host: 'developerhub-api.contentstack.com', + }); + expect(marketplaceClient).not.toHaveBeenCalledWith( + expect.objectContaining({ authtoken: 'Bearer sso-access-token' }), + ); + }); + it('should return undefined and log when error occurs', async () => { const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); mockClient.marketplace.mockReturnValue({ @@ -132,6 +157,26 @@ describe('market-app.utils', () => { expect(consoleSpy).toHaveBeenCalled(); consoleSpy.mockRestore(); }); + + it('routes SSO Bearer tokens to the SDK `authorization` option (not `authtoken`)', async () => { + mockClient.marketplace.mockReturnValue({ + app: vi.fn().mockReturnValue({ + fetch: vi.fn().mockResolvedValue({ uid: 'manifest-1' }), + }), + }); + + await getAppManifestAndAppConfig({ + organizationUid: 'org-123', + authtoken: 'Bearer sso-access-token', + region: 'NA', + manifestUid: 'manifest-1', + }); + + expect(marketplaceClient).toHaveBeenCalledWith({ + authorization: 'Bearer sso-access-token', + host: 'developerhub-api.contentstack.com', + }); + }); }); describe('fetchMarketplaceInstallationsForStack', () => { @@ -181,6 +226,25 @@ describe('market-app.utils', () => { expect(result).toEqual([matching]); }); + it('routes SSO Bearer tokens to the SDK `authorization` option (not `authtoken`)', async () => { + const fetchAll = vi.fn().mockResolvedValue({ items: [] }); + mockClient.marketplace.mockReturnValue({ + findAllApps: vi.fn(), + app: vi.fn(), + installation: vi.fn(() => ({ fetchAll })), + }); + + await fetchMarketplaceInstallationsForStack({ + ...baseParams, + authtoken: 'Bearer sso-access-token', + }); + + expect(marketplaceClient).toHaveBeenCalledWith({ + authorization: 'Bearer sso-access-token', + host: 'developerhub-api.contentstack.com', + }); + }); + it('uses fallback fetchAll() when paginated fetchAll rejects', async () => { const item = { uid: 'ins-fb', diff --git a/upload-api/src/config/index.ts b/upload-api/src/config/index.ts index 4bde1a7e..9722a13c 100644 --- a/upload-api/src/config/index.ts +++ b/upload-api/src/config/index.ts @@ -2,8 +2,11 @@ export default { plan: { dropdown: { optionLimit: 100 } }, + // CMS type configuration cmsType: process.env.CMS_TYPE || 'cmsType', isLocalPath: true, + + // AWS data configuration awsData: { awsRegion: 'us-east-2', awsAccessKeyId: '', @@ -12,6 +15,8 @@ export default { bucketName: '', bucketKey: '' }, + + // Drupal database configuration mysql: { host: process.env.MYSQL_HOST || 'host_name', user: process.env.MYSQL_USER || 'user_name', @@ -19,9 +24,13 @@ export default { database: process.env.MYSQL_DATABASE || 'database_name', port: process.env.MYSQL_PORT || 'port_number' }, + + // Drupal assets configuration assetsConfig: { base_url: process.env.DRUPAL_ASSETS_BASE_URL || 'drupal_assets_base_url', public_path: process.env.DRUPAL_ASSETS_PUBLIC_PATH || 'drupal_assets_public_path' }, + + // Local path for the CMS data localPath: process.env.CMS_LOCAL_PATH || process.env.CONTAINER_PATH || 'your_local_cms_data_path', };