From 535c03024eb4bb6fe99be4c9e8cc082e4b2d3f96 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Fri, 12 Dec 2025 23:29:41 -0500 Subject: [PATCH 1/2] mrt cloud origin and ssr improvements --- .../b2c-cli/src/commands/mrt/env/create.ts | 1 + .../b2c-cli/src/commands/mrt/env/delete.ts | 1 + .../src/commands/mrt/env/var/delete.ts | 1 + .../b2c-cli/src/commands/mrt/env/var/list.ts | 1 + .../b2c-cli/src/commands/mrt/env/var/set.ts | 1 + packages/b2c-cli/src/commands/mrt/push.ts | 3 +- packages/b2c-tooling/src/cli/config.ts | 33 +++++++++++++++---- packages/b2c-tooling/src/cli/mrt-command.ts | 20 +++++++++-- packages/b2c-tooling/src/clients/mrt.ts | 7 +++- 9 files changed, 57 insertions(+), 11 deletions(-) diff --git a/packages/b2c-cli/src/commands/mrt/env/create.ts b/packages/b2c-cli/src/commands/mrt/env/create.ts index 47fb791b..4d1dfdce 100644 --- a/packages/b2c-cli/src/commands/mrt/env/create.ts +++ b/packages/b2c-cli/src/commands/mrt/env/create.ts @@ -178,6 +178,7 @@ export default class MrtEnvCreate extends MrtCommand { externalDomain, allowCookies: allowCookies || undefined, enableSourceMaps: enableSourceMaps || undefined, + origin: this.resolvedConfig.mrtOrigin, }, this.getMrtAuth(), ); diff --git a/packages/b2c-cli/src/commands/mrt/env/delete.ts b/packages/b2c-cli/src/commands/mrt/env/delete.ts index 584b9e76..06034168 100644 --- a/packages/b2c-cli/src/commands/mrt/env/delete.ts +++ b/packages/b2c-cli/src/commands/mrt/env/delete.ts @@ -94,6 +94,7 @@ export default class MrtEnvDelete extends MrtCommand { { projectSlug: project, slug, + origin: this.resolvedConfig.mrtOrigin, }, this.getMrtAuth(), ); diff --git a/packages/b2c-cli/src/commands/mrt/env/var/delete.ts b/packages/b2c-cli/src/commands/mrt/env/var/delete.ts index 4a24a9d1..01b1756a 100644 --- a/packages/b2c-cli/src/commands/mrt/env/var/delete.ts +++ b/packages/b2c-cli/src/commands/mrt/env/var/delete.ts @@ -52,6 +52,7 @@ export default class MrtEnvVarDelete extends MrtCommand projectSlug: project, environment, key, + origin: this.resolvedConfig.mrtOrigin, }, this.getMrtAuth(), ); diff --git a/packages/b2c-cli/src/commands/mrt/env/var/list.ts b/packages/b2c-cli/src/commands/mrt/env/var/list.ts index f810d555..70f665e8 100644 --- a/packages/b2c-cli/src/commands/mrt/env/var/list.ts +++ b/packages/b2c-cli/src/commands/mrt/env/var/list.ts @@ -71,6 +71,7 @@ export default class MrtEnvVarList extends MrtCommand { { projectSlug: project, environment, + origin: this.resolvedConfig.mrtOrigin, }, this.getMrtAuth(), ); diff --git a/packages/b2c-cli/src/commands/mrt/env/var/set.ts b/packages/b2c-cli/src/commands/mrt/env/var/set.ts index 3aec23aa..a4be45fb 100644 --- a/packages/b2c-cli/src/commands/mrt/env/var/set.ts +++ b/packages/b2c-cli/src/commands/mrt/env/var/set.ts @@ -83,6 +83,7 @@ export default class MrtEnvVarSet extends MrtCommand { projectSlug: project, environment, variables, + origin: this.resolvedConfig.mrtOrigin, }, this.getMrtAuth(), ); diff --git a/packages/b2c-cli/src/commands/mrt/push.ts b/packages/b2c-cli/src/commands/mrt/push.ts index 40a08a41..0a6b8d92 100644 --- a/packages/b2c-cli/src/commands/mrt/push.ts +++ b/packages/b2c-cli/src/commands/mrt/push.ts @@ -54,7 +54,7 @@ export default class MrtPush extends MrtCommand { }), 'ssr-only': Flags.string({ description: 'Glob patterns for server-only files (comma-separated)', - default: 'ssr.js,server/**/*', + default: 'ssr.js,ssr.mjs,server/**/*', }), 'ssr-shared': Flags.string({ description: 'Glob patterns for shared files (comma-separated)', @@ -111,6 +111,7 @@ export default class MrtPush extends MrtCommand { ssrOnly, ssrShared, ssrParameters, + origin: this.resolvedConfig.mrtOrigin, }, this.getMrtAuth(), ); diff --git a/packages/b2c-tooling/src/cli/config.ts b/packages/b2c-tooling/src/cli/config.ts index 978feb20..90e83d12 100644 --- a/packages/b2c-tooling/src/cli/config.ts +++ b/packages/b2c-tooling/src/cli/config.ts @@ -24,6 +24,8 @@ export interface ResolvedConfig { mrtProject?: string; /** MRT environment name (e.g., staging, production) */ mrtEnvironment?: string; + /** MRT API origin URL override */ + mrtOrigin?: string; instanceName?: string; /** Allowed authentication methods (in priority order). If not set, all methods are allowed. */ authMethods?: AuthMethod[]; @@ -185,6 +187,7 @@ function mergeConfigs( mrtApiKey: flags.mrtApiKey, mrtProject: flags.mrtProject || dwJson.mrtProject, mrtEnvironment: flags.mrtEnvironment || dwJson.mrtEnvironment, + mrtOrigin: flags.mrtOrigin, instanceName: dwJson.instanceName || options.instance, authMethods: flags.authMethods || dwJson.authMethods, }; @@ -228,16 +231,34 @@ export interface MobifyConfigResult { * } * ``` * + * When a cloudOrigin is provided, looks for ~/.mobify--[cloudOrigin] instead. + * For example, if cloudOrigin is "https://cloud-staging.mobify.com", the file + * would be ~/.mobify--cloud-staging.mobify.com + * + * @param cloudOrigin - Optional cloud origin URL to determine which config file to read * @returns The API key and username if found, undefined otherwise */ -export function loadMobifyConfig(): MobifyConfigResult { +export function loadMobifyConfig(cloudOrigin?: string): MobifyConfigResult { const logger = getLogger(); - const mobifyPath = path.join(os.homedir(), '.mobify'); - logger.trace({path: mobifyPath}, '[Config] Checking for ~/.mobify'); + let mobifyPath: string; + if (cloudOrigin) { + // Extract hostname from origin URL for the config file suffix + try { + const url = new URL(cloudOrigin); + mobifyPath = path.join(os.homedir(), `.mobify--${url.hostname}`); + } catch { + // If URL parsing fails, use the origin as-is + mobifyPath = path.join(os.homedir(), `.mobify--${cloudOrigin}`); + } + } else { + mobifyPath = path.join(os.homedir(), '.mobify'); + } + + logger.trace({path: mobifyPath}, '[Config] Checking for mobify config'); if (!fs.existsSync(mobifyPath)) { - logger.trace('[Config] No ~/.mobify found'); + logger.trace({path: mobifyPath}, '[Config] No mobify config found'); return {}; } @@ -246,14 +267,14 @@ export function loadMobifyConfig(): MobifyConfigResult { const config = JSON.parse(content) as MobifyConfig; const hasApiKey = Boolean(config.api_key); - logger.trace({path: mobifyPath, hasApiKey, username: config.username}, '[Config] Loaded ~/.mobify'); + logger.trace({path: mobifyPath, hasApiKey, username: config.username}, '[Config] Loaded mobify config'); return { apiKey: config.api_key, username: config.username, }; } catch (error) { - logger.trace({path: mobifyPath, error}, '[Config] Failed to parse ~/.mobify'); + logger.trace({path: mobifyPath, error}, '[Config] Failed to parse mobify config'); return {}; } } diff --git a/packages/b2c-tooling/src/cli/mrt-command.ts b/packages/b2c-tooling/src/cli/mrt-command.ts index e3065655..2e0a9a77 100644 --- a/packages/b2c-tooling/src/cli/mrt-command.ts +++ b/packages/b2c-tooling/src/cli/mrt-command.ts @@ -7,6 +7,7 @@ import {ApiKeyStrategy} from '../auth/api-key.js'; import {MrtClient} from '../platform/mrt.js'; import type {MrtProject} from '../platform/mrt.js'; import {t} from '../i18n/index.js'; +import {DEFAULT_MRT_ORIGIN} from '../clients/mrt.js'; /** * Base command for Managed Runtime (MRT) operations. @@ -15,12 +16,17 @@ import {t} from '../i18n/index.js'; * API key resolution order: * 1. --api-key flag * 2. SFCC_MRT_API_KEY environment variable - * 3. ~/.mobify config file (api_key field) + * 3. ~/.mobify config file (api_key field), or ~/.mobify--[hostname] if --cloud-origin is set * * Project/environment resolution order: * 1. --project / --environment flags * 2. SFCC_MRT_PROJECT / SFCC_MRT_ENVIRONMENT environment variables * 3. dw.json (mrtProject / mrtEnvironment fields) + * + * Cloud origin resolution: + * 1. --cloud-origin flag + * 2. SFCC_MRT_CLOUD_ORIGIN environment variable + * 3. Default: https://cloud.mobify.com */ export abstract class MrtCommand extends BaseCommand { static baseFlags = { @@ -40,6 +46,10 @@ export abstract class MrtCommand extends BaseCommand extends BaseCommand = { // Flag/env takes precedence, then ~/.mobify @@ -57,6 +69,8 @@ export abstract class MrtCommand extends BaseCommand({ baseUrl: origin, From 1ff1cdc1c8d199364c15b0d09ff171d2c8690ab9 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Wed, 17 Dec 2025 16:44:16 -0500 Subject: [PATCH 2/2] mrt improvements --- .../b2c-cli/src/commands/mrt/env/create.ts | 49 +++++++++++++++++++ .../b2c-tooling/src/operations/mrt/env.ts | 17 +++++++ 2 files changed, 66 insertions(+) diff --git a/packages/b2c-cli/src/commands/mrt/env/create.ts b/packages/b2c-cli/src/commands/mrt/env/create.ts index 4d1dfdce..67ba0bc8 100644 --- a/packages/b2c-cli/src/commands/mrt/env/create.ts +++ b/packages/b2c-cli/src/commands/mrt/env/create.ts @@ -46,9 +46,48 @@ function printEnvDetails(env: MrtEnvironment, project: string): void { ui.div({text: 'Log Level:', width: labelWidth}, {text: env.log_level}); } + if (env.ssr_proxy_configs && env.ssr_proxy_configs.length > 0) { + ui.div({text: 'Proxies:', width: labelWidth}, {text: ''}); + for (const proxy of env.ssr_proxy_configs) { + const proxyPath = (proxy as {path?: string}).path ?? ''; + ui.div({text: '', width: labelWidth}, {text: ` ${proxyPath} → ${proxy.host}`}); + } + } + ux.stdout(ui.toString()); } +/** + * Proxy configuration for SSR. + */ +interface SsrProxyConfig { + host: string; + path: string; +} + +/** + * Parse a proxy string in format "path=host" into a proxy config object. + */ +function parseProxyString(proxyStr: string): SsrProxyConfig { + const eqIndex = proxyStr.indexOf('='); + if (eqIndex === -1) { + throw new Error(`Invalid proxy format: "${proxyStr}". Expected format: path=host.example.com`); + } + + const path = proxyStr.slice(0, eqIndex); + const host = proxyStr.slice(eqIndex + 1); + + if (!path) { + throw new Error(`Invalid proxy format: "${proxyStr}". Path cannot be empty.`); + } + + if (!host) { + throw new Error(`Invalid proxy format: "${proxyStr}". Host cannot be empty.`); + } + + return {path, host}; +} + /** * Valid AWS regions for MRT environments. */ @@ -99,6 +138,7 @@ export default class MrtEnvCreate extends MrtCommand { '<%= config.bin %> <%= command.id %> staging --project my-storefront --name "Staging Environment"', '<%= config.bin %> <%= command.id %> production --project my-storefront --name "Production" --production', '<%= config.bin %> <%= command.id %> feature-test -p my-storefront -n "Feature Test" --region eu-west-1', + '<%= config.bin %> <%= command.id %> staging -p my-storefront -n "Staging" --proxy api=api.example.com --proxy ocapi=ocapi.example.com', ]; static flags = { @@ -136,6 +176,10 @@ export default class MrtEnvCreate extends MrtCommand { default: false, allowNo: true, }), + proxy: Flags.string({ + description: 'Proxy configuration in format path=host (can be specified multiple times)', + multiple: true, + }), }; async run(): Promise { @@ -159,8 +203,12 @@ export default class MrtEnvCreate extends MrtCommand { 'external-domain': externalDomain, 'allow-cookies': allowCookies, 'enable-source-maps': enableSourceMaps, + proxy: proxyStrings, } = this.flags; + // Parse proxy configurations + const proxyConfigs = proxyStrings?.map((p) => parseProxyString(p)); + this.log( t('commands.mrt.env.create.creating', 'Creating environment "{{slug}}" in {{project}}...', {slug, project}), ); @@ -178,6 +226,7 @@ export default class MrtEnvCreate extends MrtCommand { externalDomain, allowCookies: allowCookies || undefined, enableSourceMaps: enableSourceMaps || undefined, + proxyConfigs, origin: this.resolvedConfig.mrtOrigin, }, this.getMrtAuth(), diff --git a/packages/b2c-tooling/src/operations/mrt/env.ts b/packages/b2c-tooling/src/operations/mrt/env.ts index b4bcceeb..18e55828 100644 --- a/packages/b2c-tooling/src/operations/mrt/env.ts +++ b/packages/b2c-tooling/src/operations/mrt/env.ts @@ -82,6 +82,17 @@ export interface CreateEnvOptions { */ whitelistedIps?: string; + /** + * Proxy configurations for SSR. + * Each proxy maps a path prefix to a backend host. + */ + proxyConfigs?: Array<{ + /** The path prefix to proxy (e.g., 'api', 'ocapi', 'einstein'). */ + path: string; + /** The backend host to proxy to (e.g., 'api.example.com'). */ + host: string; + }>; + /** * MRT API origin URL. * @default "https://cloud.mobify.com" @@ -162,6 +173,12 @@ export async function createEnv(options: CreateEnvOptions, auth: AuthStrategy): body.ssr_whitelisted_ips = options.whitelistedIps; } + if (options.proxyConfigs && options.proxyConfigs.length > 0) { + // The API accepts ssr_proxy_configs - cast to handle the path field + // which may not be in the generated types but is accepted by the API + body.ssr_proxy_configs = options.proxyConfigs as typeof body.ssr_proxy_configs; + } + const {data, error} = await client.POST('/api/projects/{project_slug}/target/', { params: { path: {project_slug: projectSlug},