Skip to content

Commit c14ddda

Browse files
authored
MRT API Improvements (#9)
* mrt cloud origin and ssr improvements * mrt improvements
1 parent 3590c0d commit c14ddda

10 files changed

Lines changed: 123 additions & 11 deletions

File tree

packages/b2c-cli/src/commands/mrt/env/create.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,48 @@ function printEnvDetails(env: MrtEnvironment, project: string): void {
4646
ui.div({text: 'Log Level:', width: labelWidth}, {text: env.log_level});
4747
}
4848

49+
if (env.ssr_proxy_configs && env.ssr_proxy_configs.length > 0) {
50+
ui.div({text: 'Proxies:', width: labelWidth}, {text: ''});
51+
for (const proxy of env.ssr_proxy_configs) {
52+
const proxyPath = (proxy as {path?: string}).path ?? '';
53+
ui.div({text: '', width: labelWidth}, {text: ` ${proxyPath}${proxy.host}`});
54+
}
55+
}
56+
4957
ux.stdout(ui.toString());
5058
}
5159

60+
/**
61+
* Proxy configuration for SSR.
62+
*/
63+
interface SsrProxyConfig {
64+
host: string;
65+
path: string;
66+
}
67+
68+
/**
69+
* Parse a proxy string in format "path=host" into a proxy config object.
70+
*/
71+
function parseProxyString(proxyStr: string): SsrProxyConfig {
72+
const eqIndex = proxyStr.indexOf('=');
73+
if (eqIndex === -1) {
74+
throw new Error(`Invalid proxy format: "${proxyStr}". Expected format: path=host.example.com`);
75+
}
76+
77+
const path = proxyStr.slice(0, eqIndex);
78+
const host = proxyStr.slice(eqIndex + 1);
79+
80+
if (!path) {
81+
throw new Error(`Invalid proxy format: "${proxyStr}". Path cannot be empty.`);
82+
}
83+
84+
if (!host) {
85+
throw new Error(`Invalid proxy format: "${proxyStr}". Host cannot be empty.`);
86+
}
87+
88+
return {path, host};
89+
}
90+
5291
/**
5392
* Valid AWS regions for MRT environments.
5493
*/
@@ -99,6 +138,7 @@ export default class MrtEnvCreate extends MrtCommand<typeof MrtEnvCreate> {
99138
'<%= config.bin %> <%= command.id %> staging --project my-storefront --name "Staging Environment"',
100139
'<%= config.bin %> <%= command.id %> production --project my-storefront --name "Production" --production',
101140
'<%= config.bin %> <%= command.id %> feature-test -p my-storefront -n "Feature Test" --region eu-west-1',
141+
'<%= config.bin %> <%= command.id %> staging -p my-storefront -n "Staging" --proxy api=api.example.com --proxy ocapi=ocapi.example.com',
102142
];
103143

104144
static flags = {
@@ -136,6 +176,10 @@ export default class MrtEnvCreate extends MrtCommand<typeof MrtEnvCreate> {
136176
default: false,
137177
allowNo: true,
138178
}),
179+
proxy: Flags.string({
180+
description: 'Proxy configuration in format path=host (can be specified multiple times)',
181+
multiple: true,
182+
}),
139183
};
140184

141185
async run(): Promise<MrtEnvironment> {
@@ -159,8 +203,12 @@ export default class MrtEnvCreate extends MrtCommand<typeof MrtEnvCreate> {
159203
'external-domain': externalDomain,
160204
'allow-cookies': allowCookies,
161205
'enable-source-maps': enableSourceMaps,
206+
proxy: proxyStrings,
162207
} = this.flags;
163208

209+
// Parse proxy configurations
210+
const proxyConfigs = proxyStrings?.map((p) => parseProxyString(p));
211+
164212
this.log(
165213
t('commands.mrt.env.create.creating', 'Creating environment "{{slug}}" in {{project}}...', {slug, project}),
166214
);
@@ -178,6 +226,8 @@ export default class MrtEnvCreate extends MrtCommand<typeof MrtEnvCreate> {
178226
externalDomain,
179227
allowCookies: allowCookies || undefined,
180228
enableSourceMaps: enableSourceMaps || undefined,
229+
proxyConfigs,
230+
origin: this.resolvedConfig.mrtOrigin,
181231
},
182232
this.getMrtAuth(),
183233
);

packages/b2c-cli/src/commands/mrt/env/delete.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export default class MrtEnvDelete extends MrtCommand<typeof MrtEnvDelete> {
9494
{
9595
projectSlug: project,
9696
slug,
97+
origin: this.resolvedConfig.mrtOrigin,
9798
},
9899
this.getMrtAuth(),
99100
);

packages/b2c-cli/src/commands/mrt/env/var/delete.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export default class MrtEnvVarDelete extends MrtCommand<typeof MrtEnvVarDelete>
5252
projectSlug: project,
5353
environment,
5454
key,
55+
origin: this.resolvedConfig.mrtOrigin,
5556
},
5657
this.getMrtAuth(),
5758
);

packages/b2c-cli/src/commands/mrt/env/var/list.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export default class MrtEnvVarList extends MrtCommand<typeof MrtEnvVarList> {
7171
{
7272
projectSlug: project,
7373
environment,
74+
origin: this.resolvedConfig.mrtOrigin,
7475
},
7576
this.getMrtAuth(),
7677
);

packages/b2c-cli/src/commands/mrt/env/var/set.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export default class MrtEnvVarSet extends MrtCommand<typeof MrtEnvVarSet> {
8383
projectSlug: project,
8484
environment,
8585
variables,
86+
origin: this.resolvedConfig.mrtOrigin,
8687
},
8788
this.getMrtAuth(),
8889
);

packages/b2c-cli/src/commands/mrt/push.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default class MrtPush extends MrtCommand<typeof MrtPush> {
5454
}),
5555
'ssr-only': Flags.string({
5656
description: 'Glob patterns for server-only files (comma-separated)',
57-
default: 'ssr.js,server/**/*',
57+
default: 'ssr.js,ssr.mjs,server/**/*',
5858
}),
5959
'ssr-shared': Flags.string({
6060
description: 'Glob patterns for shared files (comma-separated)',
@@ -111,6 +111,7 @@ export default class MrtPush extends MrtCommand<typeof MrtPush> {
111111
ssrOnly,
112112
ssrShared,
113113
ssrParameters,
114+
origin: this.resolvedConfig.mrtOrigin,
114115
},
115116
this.getMrtAuth(),
116117
);

packages/b2c-tooling/src/cli/config.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export interface ResolvedConfig {
2424
mrtProject?: string;
2525
/** MRT environment name (e.g., staging, production) */
2626
mrtEnvironment?: string;
27+
/** MRT API origin URL override */
28+
mrtOrigin?: string;
2729
instanceName?: string;
2830
/** Allowed authentication methods (in priority order). If not set, all methods are allowed. */
2931
authMethods?: AuthMethod[];
@@ -185,6 +187,7 @@ function mergeConfigs(
185187
mrtApiKey: flags.mrtApiKey,
186188
mrtProject: flags.mrtProject || dwJson.mrtProject,
187189
mrtEnvironment: flags.mrtEnvironment || dwJson.mrtEnvironment,
190+
mrtOrigin: flags.mrtOrigin,
188191
instanceName: dwJson.instanceName || options.instance,
189192
authMethods: flags.authMethods || dwJson.authMethods,
190193
};
@@ -228,16 +231,34 @@ export interface MobifyConfigResult {
228231
* }
229232
* ```
230233
*
234+
* When a cloudOrigin is provided, looks for ~/.mobify--[cloudOrigin] instead.
235+
* For example, if cloudOrigin is "https://cloud-staging.mobify.com", the file
236+
* would be ~/.mobify--cloud-staging.mobify.com
237+
*
238+
* @param cloudOrigin - Optional cloud origin URL to determine which config file to read
231239
* @returns The API key and username if found, undefined otherwise
232240
*/
233-
export function loadMobifyConfig(): MobifyConfigResult {
241+
export function loadMobifyConfig(cloudOrigin?: string): MobifyConfigResult {
234242
const logger = getLogger();
235-
const mobifyPath = path.join(os.homedir(), '.mobify');
236243

237-
logger.trace({path: mobifyPath}, '[Config] Checking for ~/.mobify');
244+
let mobifyPath: string;
245+
if (cloudOrigin) {
246+
// Extract hostname from origin URL for the config file suffix
247+
try {
248+
const url = new URL(cloudOrigin);
249+
mobifyPath = path.join(os.homedir(), `.mobify--${url.hostname}`);
250+
} catch {
251+
// If URL parsing fails, use the origin as-is
252+
mobifyPath = path.join(os.homedir(), `.mobify--${cloudOrigin}`);
253+
}
254+
} else {
255+
mobifyPath = path.join(os.homedir(), '.mobify');
256+
}
257+
258+
logger.trace({path: mobifyPath}, '[Config] Checking for mobify config');
238259

239260
if (!fs.existsSync(mobifyPath)) {
240-
logger.trace('[Config] No ~/.mobify found');
261+
logger.trace({path: mobifyPath}, '[Config] No mobify config found');
241262
return {};
242263
}
243264

@@ -246,14 +267,14 @@ export function loadMobifyConfig(): MobifyConfigResult {
246267
const config = JSON.parse(content) as MobifyConfig;
247268

248269
const hasApiKey = Boolean(config.api_key);
249-
logger.trace({path: mobifyPath, hasApiKey, username: config.username}, '[Config] Loaded ~/.mobify');
270+
logger.trace({path: mobifyPath, hasApiKey, username: config.username}, '[Config] Loaded mobify config');
250271

251272
return {
252273
apiKey: config.api_key,
253274
username: config.username,
254275
};
255276
} catch (error) {
256-
logger.trace({path: mobifyPath, error}, '[Config] Failed to parse ~/.mobify');
277+
logger.trace({path: mobifyPath, error}, '[Config] Failed to parse mobify config');
257278
return {};
258279
}
259280
}

packages/b2c-tooling/src/cli/mrt-command.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {ApiKeyStrategy} from '../auth/api-key.js';
77
import {MrtClient} from '../platform/mrt.js';
88
import type {MrtProject} from '../platform/mrt.js';
99
import {t} from '../i18n/index.js';
10+
import {DEFAULT_MRT_ORIGIN} from '../clients/mrt.js';
1011

1112
/**
1213
* Base command for Managed Runtime (MRT) operations.
@@ -15,12 +16,17 @@ import {t} from '../i18n/index.js';
1516
* API key resolution order:
1617
* 1. --api-key flag
1718
* 2. SFCC_MRT_API_KEY environment variable
18-
* 3. ~/.mobify config file (api_key field)
19+
* 3. ~/.mobify config file (api_key field), or ~/.mobify--[hostname] if --cloud-origin is set
1920
*
2021
* Project/environment resolution order:
2122
* 1. --project / --environment flags
2223
* 2. SFCC_MRT_PROJECT / SFCC_MRT_ENVIRONMENT environment variables
2324
* 3. dw.json (mrtProject / mrtEnvironment fields)
25+
*
26+
* Cloud origin resolution:
27+
* 1. --cloud-origin flag
28+
* 2. SFCC_MRT_CLOUD_ORIGIN environment variable
29+
* 3. Default: https://cloud.mobify.com
2430
*/
2531
export abstract class MrtCommand<T extends typeof Command> extends BaseCommand<T> {
2632
static baseFlags = {
@@ -40,6 +46,10 @@ export abstract class MrtCommand<T extends typeof Command> extends BaseCommand<T
4046
description: 'MRT environment (e.g., staging, production; or set mrtEnvironment in dw.json)',
4147
env: 'SFCC_MRT_ENVIRONMENT',
4248
}),
49+
'cloud-origin': Flags.string({
50+
description: `MRT cloud origin URL (default: ${DEFAULT_MRT_ORIGIN})`,
51+
env: 'SFCC_MRT_CLOUD_ORIGIN',
52+
}),
4353
};
4454

4555
protected override loadConfiguration(): ResolvedConfig {
@@ -48,15 +58,19 @@ export abstract class MrtCommand<T extends typeof Command> extends BaseCommand<T
4858
configPath: this.flags.config,
4959
};
5060

51-
// Load from ~/.mobify as fallback
52-
const mobifyConfig = loadMobifyConfig();
61+
const cloudOrigin = this.flags['cloud-origin'] as string | undefined;
62+
63+
// Load from ~/.mobify (or ~/.mobify--[hostname] if cloud-origin specified) as fallback
64+
const mobifyConfig = loadMobifyConfig(cloudOrigin);
5365

5466
const flagConfig: Partial<ResolvedConfig> = {
5567
// Flag/env takes precedence, then ~/.mobify
5668
mrtApiKey: this.flags['api-key'] || mobifyConfig.apiKey,
5769
// Project/environment from flags (if present - subclasses define these)
5870
mrtProject: this.flags.project as string | undefined,
5971
mrtEnvironment: this.flags.environment as string | undefined,
72+
// Cloud origin override
73+
mrtOrigin: cloudOrigin,
6074
};
6175

6276
return loadConfig(flagConfig, options);

packages/b2c-tooling/src/clients/mrt.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,12 @@ export const DEFAULT_MRT_ORIGIN = 'https://cloud.mobify.com';
117117
* });
118118
*/
119119
export function createMrtClient(config: MrtClientConfig, auth: AuthStrategy): MrtClient {
120-
const origin = config.origin || DEFAULT_MRT_ORIGIN;
120+
let origin = config.origin || DEFAULT_MRT_ORIGIN;
121+
122+
// Normalize origin: add https:// if no protocol specified
123+
if (origin && !origin.startsWith('http://') && !origin.startsWith('https://')) {
124+
origin = `https://${origin}`;
125+
}
121126

122127
const client = createClient<paths>({
123128
baseUrl: origin,

packages/b2c-tooling/src/operations/mrt/env.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ export interface CreateEnvOptions {
8282
*/
8383
whitelistedIps?: string;
8484

85+
/**
86+
* Proxy configurations for SSR.
87+
* Each proxy maps a path prefix to a backend host.
88+
*/
89+
proxyConfigs?: Array<{
90+
/** The path prefix to proxy (e.g., 'api', 'ocapi', 'einstein'). */
91+
path: string;
92+
/** The backend host to proxy to (e.g., 'api.example.com'). */
93+
host: string;
94+
}>;
95+
8596
/**
8697
* MRT API origin URL.
8798
* @default "https://cloud.mobify.com"
@@ -162,6 +173,12 @@ export async function createEnv(options: CreateEnvOptions, auth: AuthStrategy):
162173
body.ssr_whitelisted_ips = options.whitelistedIps;
163174
}
164175

176+
if (options.proxyConfigs && options.proxyConfigs.length > 0) {
177+
// The API accepts ssr_proxy_configs - cast to handle the path field
178+
// which may not be in the generated types but is accepted by the API
179+
body.ssr_proxy_configs = options.proxyConfigs as typeof body.ssr_proxy_configs;
180+
}
181+
165182
const {data, error} = await client.POST('/api/projects/{project_slug}/target/', {
166183
params: {
167184
path: {project_slug: projectSlug},

0 commit comments

Comments
 (0)