@salesforce/storefront-next-runtime 0.2.0 → 0.3.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/DesignFrame.js +6 -2
  2. package/dist/DesignFrame.js.map +1 -1
  3. package/dist/DesignRegion.js +2 -1
  4. package/dist/DesignRegion.js.map +1 -1
  5. package/dist/component.types.d.ts +6 -0
  6. package/dist/component.types.d.ts.map +1 -1
  7. package/dist/config-load.d.ts +27 -0
  8. package/dist/config-load.d.ts.map +1 -0
  9. package/dist/config-load.js +3 -0
  10. package/dist/config.d.ts +248 -1
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +429 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/design-data.d.ts +40 -27
  15. package/dist/design-data.d.ts.map +1 -1
  16. package/dist/design-data.js +50 -26
  17. package/dist/design-data.js.map +1 -1
  18. package/dist/design-react-core.d.ts +2 -2
  19. package/dist/design-react-core.js +3 -1
  20. package/dist/design-react-core.js.map +1 -1
  21. package/dist/events.d.ts +9 -4
  22. package/dist/events.d.ts.map +1 -1
  23. package/dist/events.js +6 -6
  24. package/dist/events.js.map +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/load-config.js +41 -0
  27. package/dist/load-config.js.map +1 -0
  28. package/dist/multi-site.d.ts +68 -43
  29. package/dist/multi-site.d.ts.map +1 -1
  30. package/dist/multi-site.js +36 -10
  31. package/dist/multi-site.js.map +1 -1
  32. package/dist/routing.d.ts.map +1 -1
  33. package/dist/routing.js +4 -37
  34. package/dist/routing.js.map +1 -1
  35. package/dist/scapi.d.ts +8 -0
  36. package/dist/scapi.d.ts.map +1 -1
  37. package/dist/scapi.js +1 -1
  38. package/dist/scapi.js.map +1 -1
  39. package/dist/schema.d.ts +78 -0
  40. package/dist/schema.d.ts.map +1 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +7 -1
package/dist/config.js CHANGED
@@ -0,0 +1,429 @@
1
+ import { createContext, useContext } from "react";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { createContext as createContext$1 } from "react-router";
4
+
5
+ //#region src/config/utils.ts
6
+ /**
7
+ * Copyright 2026 Salesforce, Inc.
8
+ *
9
+ * Licensed under the Apache License, Version 2.0 (the "License");
10
+ * you may not use this file except in compliance with the License.
11
+ * You may obtain a copy of the License at
12
+ *
13
+ * http://www.apache.org/licenses/LICENSE-2.0
14
+ *
15
+ * Unless required by applicable law or agreed to in writing, software
16
+ * distributed under the License is distributed on an "AS IS" BASIS,
17
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ * See the License for the specific language governing permissions and
19
+ * limitations under the License.
20
+ */
21
+ /**
22
+ * Type guard to check if value is a plain object (not array, null, or other types)
23
+ */
24
+ const isPlainObject = (value) => {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ };
27
+ /**
28
+ * Deep merge two objects, with source values overriding target values
29
+ * Arrays are replaced, not merged
30
+ *
31
+ * @param target - The base object
32
+ * @param source - The object with values to merge in
33
+ * @returns A new merged object
34
+ *
35
+ * @example
36
+ * deepMerge(
37
+ * { a: { b: 1, c: 2 } },
38
+ * { a: { b: 3, d: 4 } }
39
+ * )
40
+ * // Returns: { a: { b: 3, c: 2, d: 4 } }
41
+ */
42
+ const deepMerge = (target, source) => {
43
+ const result = { ...target };
44
+ for (const key in source) {
45
+ const sourceValue = source[key];
46
+ const targetValue = result[key];
47
+ if (isPlainObject(sourceValue) && isPlainObject(targetValue)) result[key] = deepMerge(targetValue, sourceValue);
48
+ else result[key] = sourceValue;
49
+ }
50
+ return result;
51
+ };
52
+ /**
53
+ * Convert a path string with double underscore separators to a nested object
54
+ * Normalizes keys to match baseConfig casing (case-insensitive lookup, preserves baseConfig case)
55
+ *
56
+ * @param path - The path string (e.g., 'app__pages__cart__quantityUpdateDebounce')
57
+ * @param value - The value to set at the path
58
+ * @param baseConfig - Optional base config for case normalization
59
+ * @returns A nested object
60
+ *
61
+ * @example
62
+ * pathToObject('app__pages__cart__maxQuantity', 999)
63
+ * // Returns: { app: { pages: { cart: { maxQuantity: 999 } } } }
64
+ *
65
+ * @example
66
+ * // With baseConfig normalization:
67
+ * pathToObject('APP__SITE__LOCALE', 'en-GB', { app: { site: { locale: 'en-GB' } } })
68
+ * // Returns: { app: { site: { locale: 'en-GB' } } } (normalized to baseConfig casing)
69
+ */
70
+ const pathToObject = (path, value, baseConfig) => {
71
+ const keys = path.split("__");
72
+ const result = {};
73
+ let current = result;
74
+ let configCurrent = baseConfig;
75
+ for (let i = 0; i < keys.length - 1; i++) {
76
+ const key = keys[i];
77
+ let normalizedKey = key;
78
+ if (configCurrent && typeof configCurrent === "object" && !Array.isArray(configCurrent)) {
79
+ const actualKey = Object.keys(configCurrent).find((k) => k.toLowerCase() === key.toLowerCase());
80
+ if (actualKey) {
81
+ normalizedKey = actualKey;
82
+ configCurrent = configCurrent[actualKey];
83
+ } else configCurrent = null;
84
+ }
85
+ current[normalizedKey] = {};
86
+ current = current[normalizedKey];
87
+ }
88
+ const lastKey = keys[keys.length - 1];
89
+ let normalizedLastKey = lastKey;
90
+ if (configCurrent && typeof configCurrent === "object" && !Array.isArray(configCurrent)) {
91
+ const actualKey = Object.keys(configCurrent).find((k) => k.toLowerCase() === lastKey.toLowerCase());
92
+ if (actualKey) normalizedLastKey = actualKey;
93
+ }
94
+ current[normalizedLastKey] = value;
95
+ return result;
96
+ };
97
+ /**
98
+ * Parse environment variable value with optimistic JSON parsing
99
+ * Tries to parse as JSON first, falls back to string if invalid
100
+ * Supports multi-line formatted JSON by normalizing whitespace before parsing
101
+ *
102
+ * @param varValue - The environment variable value
103
+ * @param varName - Optional variable name for better error messages
104
+ * @returns The parsed value (JSON type if valid JSON, otherwise string)
105
+ *
106
+ * @example
107
+ * // Primitives
108
+ * parseEnvValue('42') // → 42 (number)
109
+ * parseEnvValue('true') // → true (boolean)
110
+ * parseEnvValue('hello') // → 'hello' (string)
111
+ *
112
+ * @example
113
+ * // Single-line JSON
114
+ * parseEnvValue('["Apple","Google"]') // → ['Apple', 'Google'] (array)
115
+ * parseEnvValue('{"key":"value"}') // → {key: 'value'} (object)
116
+ *
117
+ * @example
118
+ * // Multi-line formatted JSON (whitespace normalized automatically)
119
+ * parseEnvValue('[
120
+ * {"id": "en-GB"},
121
+ * {"id": "fr-FR"}
122
+ * ]') // → [{id: 'en-GB'}, {id: 'fr-FR'}] (array)
123
+ */
124
+ const parseEnvValue = (varValue, varName) => {
125
+ try {
126
+ return JSON.parse(varValue);
127
+ } catch {
128
+ const trimmed = varValue.trim();
129
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) try {
130
+ const normalized = varValue.replace(/\s+/g, " ").trim();
131
+ return JSON.parse(normalized);
132
+ } catch {
133
+ if (process.env.NODE_ENV === "development") {
134
+ const preview = varValue.length > 50 ? `${varValue.substring(0, 50)}...` : varValue;
135
+ const varInfo = varName ? ` in "${varName}"` : "";
136
+ console.warn(`[Config Warning] Value${varInfo} looks like JSON but failed to parse: "${preview}". Using as string instead. Check for syntax errors if this was meant to be JSON.`);
137
+ }
138
+ }
139
+ return varValue;
140
+ }
141
+ };
142
+ /**
143
+ * Extract all valid paths from a config object (recursively traverses the object structure)
144
+ * Returns paths in lowercase with double underscore separators
145
+ *
146
+ * @param obj - The config object to extract paths from
147
+ * @param prefix - Current path prefix (used for recursion)
148
+ * @returns Array of valid config paths
149
+ *
150
+ * @example
151
+ * extractValidPaths({ app: { site: { locale: 'en-GB' } } })
152
+ * // Returns: ['app__site__locale']
153
+ */
154
+ const extractValidPaths = (obj, prefix = "") => {
155
+ if (!isPlainObject(obj)) return prefix ? [prefix] : [];
156
+ const paths = [];
157
+ for (const [key, value] of Object.entries(obj)) {
158
+ const normalizedKey = key.toLowerCase();
159
+ const currentPath = prefix ? `${prefix}__${normalizedKey}` : normalizedKey;
160
+ if (isPlainObject(value)) {
161
+ paths.push(currentPath);
162
+ paths.push(...extractValidPaths(value, currentPath));
163
+ } else paths.push(currentPath);
164
+ }
165
+ return paths;
166
+ };
167
+ /**
168
+ * Merge environment variables with PUBLIC__ prefix into config.
169
+ *
170
+ * Uses double underscore (__) to target nested config paths.
171
+ * All PUBLIC__ prefixed variables are exposed to the client (bundled into window.__APP_CONFIG__).
172
+ *
173
+ * Server-only secrets should NEVER use this — read them directly from process.env in server code.
174
+ *
175
+ * Environment variables:
176
+ * - `PUBLIC__<path>` (optional): Override any config path. e.g. `PUBLIC__app__commerce__api__clientId=abc123`
177
+ * - `NODE_ENV` (optional): When set to 'development', enables conflict warnings for overlapping paths
178
+ *
179
+ * @param env - Environment variables object (defaults to process.env)
180
+ * @param baseConfig - Optional base config for strict path validation and case normalization
181
+ * @param options - Optional configuration including protected paths
182
+ * @returns Object with overrides to merge into base config
183
+ *
184
+ * @example
185
+ * // Environment variables:
186
+ * // PUBLIC__app__commerce__api__clientId=abc123
187
+ * // PUBLIC__app__pages__cart__quantityUpdateDebounce=1000
188
+ * // PUBLIC__app__features__socialLogin__providers=["Apple","Google"]
189
+ *
190
+ * mergeEnvConfig()
191
+ * // Returns:
192
+ * // {
193
+ * // app: {
194
+ * // commerce: { api: { clientId: 'abc123' } },
195
+ * // pages: { cart: { quantityUpdateDebounce: 1000 } },
196
+ * // features: { socialLogin: { providers: ['Apple', 'Google'] } }
197
+ * // }
198
+ * // }
199
+ */
200
+ const mergeEnvConfig = (env = typeof process !== "undefined" ? process.env : {}, baseConfig, options) => {
201
+ const PUBLIC_PREFIX = "PUBLIC__";
202
+ const MAX_VAR_NAME_LENGTH = 512;
203
+ const MAX_TOTAL_VALUE_SIZE = 32 * 1024;
204
+ const MAX_DEPTH = 10;
205
+ const protectedPaths = options?.protectedPaths ?? [];
206
+ const validPaths = baseConfig ? extractValidPaths(baseConfig) : [];
207
+ const envVars = [];
208
+ let totalValueSize = 0;
209
+ for (const [varName, varValue] of Object.entries(env)) {
210
+ if (varValue === void 0 || varValue === null || !varName.startsWith(PUBLIC_PREFIX)) continue;
211
+ if (varName.length > MAX_VAR_NAME_LENGTH) throw new Error(`Environment variable name "${varName}" exceeds MRT limit of ${MAX_VAR_NAME_LENGTH} characters. Current length: ${varName.length} characters. Consider using shorter paths or consolidating configuration using JSON values.`);
212
+ const path = varName.substring(8);
213
+ if (!path) throw new Error(`Invalid environment variable "${varName}": Path cannot be empty after PUBLIC__ prefix. Expected format: PUBLIC__path__to__value (e.g., PUBLIC__app__site__locale)`);
214
+ const depth = path.split("__").length;
215
+ if (depth > MAX_DEPTH) throw new Error(`Environment variable "${varName}" exceeds maximum path depth of ${MAX_DEPTH}. Current depth: ${depth}. Consider consolidating with JSON values or reducing nesting levels.`);
216
+ const normalizedPath = path.toLowerCase();
217
+ if (protectedPaths.some((protectedPath) => normalizedPath === protectedPath || normalizedPath.startsWith(`${protectedPath}__`))) throw new Error(`Environment variable "${varName}" attempts to override protected config path "${path}".\n\nThe engagement configuration cannot be overridden via environment variables. Update config.server.ts directly to change engagement settings.`);
218
+ if (baseConfig && validPaths.length > 0) {
219
+ if (!validPaths.includes(normalizedPath)) throw new Error(`Invalid environment variable "${varName}": Config path "${path}" does not exist in config.server.ts.\n\nCheck your config.server.ts for available configuration paths, or add this path to your base configuration.`);
220
+ }
221
+ totalValueSize += varValue.length;
222
+ envVars.push({
223
+ name: varName,
224
+ path,
225
+ value: varValue,
226
+ depth: path.split("__").length
227
+ });
228
+ }
229
+ if (totalValueSize > MAX_TOTAL_VALUE_SIZE) throw new Error(`Total size of PUBLIC__ environment variable values exceeds MRT limit of ${MAX_TOTAL_VALUE_SIZE} bytes (32 KB). Current size: ${totalValueSize} bytes. Consider consolidating configuration using JSON values to reduce the number of variables, or move non-essential configuration to defaults in config.server.ts.`);
230
+ envVars.sort((a, b) => a.depth - b.depth);
231
+ const conflicts = [];
232
+ for (let i = 0; i < envVars.length; i++) for (let j = i + 1; j < envVars.length; j++) {
233
+ const shorter = envVars[i].path;
234
+ if (envVars[j].path.startsWith(`${shorter}__`)) conflicts.push({
235
+ parent: envVars[i].name,
236
+ child: envVars[j].name
237
+ });
238
+ }
239
+ if (conflicts.length > 0 && process.env.NODE_ENV === "development") console.warn(`[Config Warning] Conflicting environment variables detected. More specific paths will override parent paths:\n${conflicts.map((c) => ` ${c.parent} ← overridden by → ${c.child}`).join("\n")}`);
240
+ let merged = {};
241
+ for (const envVar of envVars) try {
242
+ const parsedValue = parseEnvValue(envVar.value, envVar.name);
243
+ const pathObject = pathToObject(envVar.path, parsedValue, baseConfig);
244
+ merged = deepMerge(merged, pathObject);
245
+ } catch (error) {
246
+ throw new Error(`Failed to process environment variable "${envVar.name}" with value "${envVar.value}": ${error instanceof Error ? error.message : String(error)}`);
247
+ }
248
+ return merged;
249
+ };
250
+
251
+ //#endregion
252
+ //#region src/config/schema.ts
253
+ /**
254
+ * Define a type-safe storefront configuration with IDE autocomplete.
255
+ *
256
+ * Automatically merges `PUBLIC__` prefixed environment variables into the config
257
+ * at load time. Validates env vars against the base config structure (strict mode —
258
+ * only allows overriding existing paths).
259
+ *
260
+ * Environment variables:
261
+ * - `PUBLIC__<path>` (optional): Override any config path using double underscore separators.
262
+ * e.g. `PUBLIC__app__commerce__api__clientId=abc123` maps to `config.app.commerce.api.clientId`
263
+ * - `PUBLIC__app__pages__cart__quantityUpdateDebounce=1000` maps to a number (optimistic JSON parsing)
264
+ * - `PUBLIC__app__features__socialLogin__providers=["Apple","Google"]` maps to an array
265
+ *
266
+ * @param config - The base configuration object with all defaults
267
+ * @param options - Optional settings (e.g., protectedPaths to prevent env var overrides)
268
+ * @returns The config with environment variable overrides merged in
269
+ *
270
+ * @example
271
+ * // In config.server.ts:
272
+ * import { defineConfig } from '@salesforce/storefront-next-runtime/config';
273
+ *
274
+ * export default defineConfig({
275
+ * metadata: { projectName: 'My Store', projectSlug: 'my-store' },
276
+ * app: {
277
+ * commerce: { api: { clientId: '', organizationId: '', shortCode: '' }, sites: [] },
278
+ * defaultSiteId: 'RefArch',
279
+ * },
280
+ * }, { protectedPaths: ['app__engagement'] });
281
+ */
282
+ function defineConfig(config, options) {
283
+ return deepMerge(config, mergeEnvConfig(process.env, config, { protectedPaths: options?.protectedPaths }));
284
+ }
285
+
286
+ //#endregion
287
+ //#region src/config/context.tsx
288
+ /**
289
+ * Router context for application configuration.
290
+ *
291
+ * Populated by `createAppConfigMiddleware` with the `app` section of config.
292
+ * Accessible in loaders, actions, and middleware via `context.get(appConfigContext)`.
293
+ */
294
+ const appConfigContext = createContext$1();
295
+ /**
296
+ * React context for application configuration.
297
+ *
298
+ * Used by the `useConfig()` hook in React components.
299
+ * Populated by `ConfigProvider` in the component tree.
300
+ */
301
+ const ConfigContext = createContext(null);
302
+ /**
303
+ * Extract the `app` section from a full config object.
304
+ *
305
+ * @param staticConfig - The full config object (output of `defineConfig()`)
306
+ * @returns The `app` section of the config
307
+ */
308
+ function createAppConfig(staticConfig) {
309
+ return staticConfig.app;
310
+ }
311
+ /**
312
+ * React context provider for application configuration.
313
+ *
314
+ * Wrap your component tree with this to enable `useConfig()` in child components.
315
+ * Typically placed in the root layout component.
316
+ */
317
+ function ConfigProvider({ config, children }) {
318
+ return /* @__PURE__ */ jsx(ConfigContext.Provider, {
319
+ value: config,
320
+ children
321
+ });
322
+ }
323
+
324
+ //#endregion
325
+ //#region src/config/get-config.ts
326
+ /**
327
+ * Get configuration in loaders, actions, and utilities.
328
+ *
329
+ * Pass context parameter in server loaders/actions.
330
+ * Omit context parameter in client loaders (uses window.__APP_CONFIG__).
331
+ *
332
+ * @param context - Router context for server loaders/actions
333
+ * @returns App configuration
334
+ */
335
+ function getConfig(context) {
336
+ if (context) {
337
+ const config = context.get(appConfigContext);
338
+ if (!config) throw new Error("Configuration not available in router context. Ensure appConfigMiddleware.server runs before other middleware.");
339
+ return config;
340
+ }
341
+ if (typeof window !== "undefined" && window.__APP_CONFIG__) return window.__APP_CONFIG__;
342
+ throw new Error("Configuration not available. This can happen if:\n1. Server: Pass context parameter: getConfig(context)\n2. Client: Ensure window.__APP_CONFIG__ was injected during SSR\n3. React component: Use useConfig() hook instead of getConfig()");
343
+ }
344
+ /**
345
+ * Get configuration in React components.
346
+ *
347
+ * Must use this hook (not getConfig) because React Context requires useContext().
348
+ *
349
+ * @returns App configuration
350
+ */
351
+ function useConfig() {
352
+ const config = useContext(ConfigContext);
353
+ if (!config) throw new Error("useConfig must be used within ConfigProvider. Ensure ConfigProvider wraps your component tree in root.tsx");
354
+ return config;
355
+ }
356
+
357
+ //#endregion
358
+ //#region src/config/middleware.ts
359
+ /**
360
+ * Create app config middleware for both server and client.
361
+ *
362
+ * Follows the same factory pattern as `createMultiSiteMiddleware`.
363
+ *
364
+ * The server middleware:
365
+ * - Validates required Commerce API fields on first request (one-time)
366
+ * - Sets `appConfigContext` in router context with `config.app`
367
+ *
368
+ * The client middleware:
369
+ * - Reads `window.__APP_CONFIG__` (injected during SSR)
370
+ * - Sets `appConfigContext` in router context
371
+ *
372
+ * Environment variables:
373
+ * - `SCAPI_PROXY_HOST` (optional): When set, skips `shortCode` validation
374
+ * (workspace environments route through a proxy that doesn't require shortCode)
375
+ * - `NODE_ENV` (optional): When set to 'test', skips validation entirely
376
+ *
377
+ * @param config - The full config object (output of `defineConfig()`)
378
+ * @returns Object with `server` and `client` middleware functions
379
+ *
380
+ * @example
381
+ * import { createAppConfigMiddleware } from '@salesforce/storefront-next-runtime/config';
382
+ * import config from '@/config/server';
383
+ *
384
+ * const appConfigMiddleware = createAppConfigMiddleware(config);
385
+ *
386
+ * export const middleware = [appConfigMiddleware.server, ...otherMiddleware];
387
+ * export const clientMiddleware = [appConfigMiddleware.client, ...otherClientMiddleware];
388
+ */
389
+ function createAppConfigMiddleware(config) {
390
+ let validationRun = false;
391
+ function validateConfig() {
392
+ if (validationRun || process.env.NODE_ENV === "test") return;
393
+ const api = config.app.commerce?.api;
394
+ const required = {
395
+ clientId: api?.clientId ?? "",
396
+ organizationId: api?.organizationId ?? ""
397
+ };
398
+ if (!process.env.SCAPI_PROXY_HOST) required.shortCode = api?.shortCode ?? "";
399
+ const missing = Object.entries(required).filter(([_, value]) => !value).map(([key]) => key);
400
+ if (missing.length > 0) {
401
+ const envVarMap = {
402
+ clientId: "PUBLIC__app__commerce__api__clientId",
403
+ organizationId: "PUBLIC__app__commerce__api__organizationId",
404
+ shortCode: "PUBLIC__app__commerce__api__shortCode"
405
+ };
406
+ throw new Error(`Missing required Commerce API configuration: ${missing.join(", ")}\n\nSet these environment variables in your MRT deployment or .env file:\n${missing.map((key) => ` ${envVarMap[key]}=your-value`).join("\n")}\n\nExample .env file:\nPUBLIC__app__commerce__api__clientId=your-client-id\nPUBLIC__app__commerce__api__organizationId=your-org-id\nPUBLIC__app__commerce__api__shortCode=your-short-code\n\nSee docs/README-CONFIG.md for complete configuration documentation.`);
407
+ }
408
+ validationRun = true;
409
+ }
410
+ const server = ({ context }, next) => {
411
+ validateConfig();
412
+ context.set(appConfigContext, config.app);
413
+ return next();
414
+ };
415
+ const client = async ({ context }, next) => {
416
+ const appConfig = typeof window !== "undefined" ? window.__APP_CONFIG__ : void 0;
417
+ if (!appConfig) throw new Error("window.__APP_CONFIG__ not available. Check that server loader is injecting config into HTML via Layout component.");
418
+ context.set(appConfigContext, appConfig);
419
+ return next();
420
+ };
421
+ return {
422
+ server,
423
+ client
424
+ };
425
+ }
426
+
427
+ //#endregion
428
+ export { ConfigContext, ConfigProvider, appConfigContext, createAppConfig, createAppConfigMiddleware, deepMerge, defineConfig, extractValidPaths, getConfig, mergeEnvConfig, parseEnvValue, pathToObject, useConfig };
429
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","names":["result: Record<string, unknown>","configCurrent: unknown","paths: string[]","envVars: EnvVar[]","conflicts: Array<{ parent: string; child: string }>","merged: Record<string, unknown>","createRouterContext","required: Record<string, string>","envVarMap: Record<string, string>","server: MiddlewareFunction<Response>","client: MiddlewareFunction<Record<string, unknown>>"],"sources":["../src/config/utils.ts","../src/config/schema.ts","../src/config/context.tsx","../src/config/get-config.ts","../src/config/middleware.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Type guard to check if value is a plain object (not array, null, or other types)\n */\nconst isPlainObject = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n};\n\n/**\n * Deep merge two objects, with source values overriding target values\n * Arrays are replaced, not merged\n *\n * @param target - The base object\n * @param source - The object with values to merge in\n * @returns A new merged object\n *\n * @example\n * deepMerge(\n * { a: { b: 1, c: 2 } },\n * { a: { b: 3, d: 4 } }\n * )\n * // Returns: { a: { b: 3, c: 2, d: 4 } }\n */\nexport const deepMerge = <T extends Record<string, unknown>>(target: T, source: Record<string, unknown>): T => {\n const result: Record<string, unknown> = { ...target };\n\n for (const key in source) {\n const sourceValue = source[key];\n const targetValue = result[key];\n\n if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {\n result[key] = deepMerge(targetValue, sourceValue);\n } else {\n result[key] = sourceValue;\n }\n }\n\n return result as T;\n};\n\n/**\n * Convert a path string with double underscore separators to a nested object\n * Normalizes keys to match baseConfig casing (case-insensitive lookup, preserves baseConfig case)\n *\n * @param path - The path string (e.g., 'app__pages__cart__quantityUpdateDebounce')\n * @param value - The value to set at the path\n * @param baseConfig - Optional base config for case normalization\n * @returns A nested object\n *\n * @example\n * pathToObject('app__pages__cart__maxQuantity', 999)\n * // Returns: { app: { pages: { cart: { maxQuantity: 999 } } } }\n *\n * @example\n * // With baseConfig normalization:\n * pathToObject('APP__SITE__LOCALE', 'en-GB', { app: { site: { locale: 'en-GB' } } })\n * // Returns: { app: { site: { locale: 'en-GB' } } } (normalized to baseConfig casing)\n */\nexport const pathToObject = (\n path: string,\n value: unknown,\n baseConfig?: Record<string, unknown>\n): Record<string, unknown> => {\n const keys = path.split('__');\n const result: Record<string, unknown> = {};\n\n let current = result;\n let configCurrent: unknown = baseConfig;\n\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i];\n\n let normalizedKey = key;\n if (configCurrent && typeof configCurrent === 'object' && !Array.isArray(configCurrent)) {\n const actualKey = Object.keys(configCurrent).find((k) => k.toLowerCase() === key.toLowerCase());\n if (actualKey) {\n normalizedKey = actualKey;\n configCurrent = (configCurrent as Record<string, unknown>)[actualKey];\n } else {\n configCurrent = null;\n }\n }\n\n current[normalizedKey] = {};\n current = current[normalizedKey] as Record<string, unknown>;\n }\n\n const lastKey = keys[keys.length - 1];\n let normalizedLastKey = lastKey;\n if (configCurrent && typeof configCurrent === 'object' && !Array.isArray(configCurrent)) {\n const actualKey = Object.keys(configCurrent).find((k) => k.toLowerCase() === lastKey.toLowerCase());\n if (actualKey) {\n normalizedLastKey = actualKey;\n }\n }\n\n current[normalizedLastKey] = value;\n return result;\n};\n\n/**\n * Parse environment variable value with optimistic JSON parsing\n * Tries to parse as JSON first, falls back to string if invalid\n * Supports multi-line formatted JSON by normalizing whitespace before parsing\n *\n * @param varValue - The environment variable value\n * @param varName - Optional variable name for better error messages\n * @returns The parsed value (JSON type if valid JSON, otherwise string)\n *\n * @example\n * // Primitives\n * parseEnvValue('42') // → 42 (number)\n * parseEnvValue('true') // → true (boolean)\n * parseEnvValue('hello') // → 'hello' (string)\n *\n * @example\n * // Single-line JSON\n * parseEnvValue('[\"Apple\",\"Google\"]') // → ['Apple', 'Google'] (array)\n * parseEnvValue('{\"key\":\"value\"}') // → {key: 'value'} (object)\n *\n * @example\n * // Multi-line formatted JSON (whitespace normalized automatically)\n * parseEnvValue('[\n * {\"id\": \"en-GB\"},\n * {\"id\": \"fr-FR\"}\n * ]') // → [{id: 'en-GB'}, {id: 'fr-FR'}] (array)\n */\nexport const parseEnvValue = (varValue: string, varName?: string): unknown => {\n try {\n return JSON.parse(varValue);\n } catch {\n const trimmed = varValue.trim();\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n try {\n const normalized = varValue.replace(/\\s+/g, ' ').trim();\n return JSON.parse(normalized);\n } catch {\n if (process.env.NODE_ENV === 'development') {\n const preview = varValue.length > 50 ? `${varValue.substring(0, 50)}...` : varValue;\n const varInfo = varName ? ` in \"${varName}\"` : '';\n // eslint-disable-next-line no-console\n console.warn(\n `[Config Warning] Value${varInfo} looks like JSON but failed to parse: \"${preview}\". ` +\n `Using as string instead. Check for syntax errors if this was meant to be JSON.`\n );\n }\n }\n }\n return varValue;\n }\n};\n\n/**\n * Extract all valid paths from a config object (recursively traverses the object structure)\n * Returns paths in lowercase with double underscore separators\n *\n * @param obj - The config object to extract paths from\n * @param prefix - Current path prefix (used for recursion)\n * @returns Array of valid config paths\n *\n * @example\n * extractValidPaths({ app: { site: { locale: 'en-GB' } } })\n * // Returns: ['app__site__locale']\n */\nexport const extractValidPaths = (obj: unknown, prefix = ''): string[] => {\n if (!isPlainObject(obj)) {\n return prefix ? [prefix] : [];\n }\n\n const paths: string[] = [];\n for (const [key, value] of Object.entries(obj)) {\n const normalizedKey = key.toLowerCase();\n const currentPath = prefix ? `${prefix}__${normalizedKey}` : normalizedKey;\n\n if (isPlainObject(value)) {\n paths.push(currentPath); // Allow setting whole object (e.g. PUBLIC__app__commerceAgent)\n // Recursively extract paths from nested objects\n paths.push(...extractValidPaths(value, currentPath));\n } else {\n // Leaf node - this is a valid config path\n paths.push(currentPath);\n }\n }\n\n return paths;\n};\n\ninterface EnvVar {\n name: string;\n path: string;\n value: string;\n depth: number;\n}\n\n/**\n * Options for mergeEnvConfig\n */\nexport interface MergeEnvConfigOptions {\n /**\n * Config paths that cannot be overridden by environment variables.\n * Paths are matched case-insensitively with double underscore separators.\n * Any env var targeting a protected path or a sub-path of it will throw an error.\n *\n * @example ['app__engagement'] — prevents PUBLIC__app__engagement__* from being set via env\n */\n protectedPaths?: string[];\n}\n\n/**\n * Merge environment variables with PUBLIC__ prefix into config.\n *\n * Uses double underscore (__) to target nested config paths.\n * All PUBLIC__ prefixed variables are exposed to the client (bundled into window.__APP_CONFIG__).\n *\n * Server-only secrets should NEVER use this — read them directly from process.env in server code.\n *\n * Environment variables:\n * - `PUBLIC__<path>` (optional): Override any config path. e.g. `PUBLIC__app__commerce__api__clientId=abc123`\n * - `NODE_ENV` (optional): When set to 'development', enables conflict warnings for overlapping paths\n *\n * @param env - Environment variables object (defaults to process.env)\n * @param baseConfig - Optional base config for strict path validation and case normalization\n * @param options - Optional configuration including protected paths\n * @returns Object with overrides to merge into base config\n *\n * @example\n * // Environment variables:\n * // PUBLIC__app__commerce__api__clientId=abc123\n * // PUBLIC__app__pages__cart__quantityUpdateDebounce=1000\n * // PUBLIC__app__features__socialLogin__providers=[\"Apple\",\"Google\"]\n *\n * mergeEnvConfig()\n * // Returns:\n * // {\n * // app: {\n * // commerce: { api: { clientId: 'abc123' } },\n * // pages: { cart: { quantityUpdateDebounce: 1000 } },\n * // features: { socialLogin: { providers: ['Apple', 'Google'] } }\n * // }\n * // }\n */\nexport const mergeEnvConfig = (\n env: Record<string, string | undefined> = typeof process !== 'undefined' ? process.env : {},\n baseConfig?: Record<string, unknown>,\n options?: MergeEnvConfigOptions\n): Record<string, unknown> => {\n const PUBLIC_PREFIX = 'PUBLIC__';\n const MAX_VAR_NAME_LENGTH = 512; // MRT limit: 512 characters\n const MAX_TOTAL_VALUE_SIZE = 32 * 1024; // MRT limit: 32 KB\n const MAX_DEPTH = 10;\n\n const protectedPaths = options?.protectedPaths ?? [];\n const validPaths = baseConfig ? extractValidPaths(baseConfig) : [];\n\n const envVars: EnvVar[] = [];\n let totalValueSize = 0;\n\n for (const [varName, varValue] of Object.entries(env)) {\n if (varValue === undefined || varValue === null || !varName.startsWith(PUBLIC_PREFIX)) continue;\n\n if (varName.length > MAX_VAR_NAME_LENGTH) {\n throw new Error(\n `Environment variable name \"${varName}\" exceeds MRT limit of ${MAX_VAR_NAME_LENGTH} characters. ` +\n `Current length: ${varName.length} characters. ` +\n `Consider using shorter paths or consolidating configuration using JSON values.`\n );\n }\n\n const path = varName.substring(PUBLIC_PREFIX.length);\n\n if (!path) {\n throw new Error(\n `Invalid environment variable \"${varName}\": Path cannot be empty after PUBLIC__ prefix. ` +\n `Expected format: PUBLIC__path__to__value (e.g., PUBLIC__app__site__locale)`\n );\n }\n\n const depth = path.split('__').length;\n if (depth > MAX_DEPTH) {\n throw new Error(\n `Environment variable \"${varName}\" exceeds maximum path depth of ${MAX_DEPTH}. ` +\n `Current depth: ${depth}. ` +\n `Consider consolidating with JSON values or reducing nesting levels.`\n );\n }\n\n const normalizedPath = path.toLowerCase();\n const isProtected = protectedPaths.some(\n (protectedPath) => normalizedPath === protectedPath || normalizedPath.startsWith(`${protectedPath}__`)\n );\n\n if (isProtected) {\n throw new Error(\n `Environment variable \"${varName}\" attempts to override protected config path \"${path}\".\\n\\n` +\n `The engagement configuration cannot be overridden via environment variables. ` +\n `Update config.server.ts directly to change engagement settings.`\n );\n }\n\n if (baseConfig && validPaths.length > 0) {\n if (!validPaths.includes(normalizedPath)) {\n throw new Error(\n `Invalid environment variable \"${varName}\": Config path \"${path}\" does not exist in config.server.ts.\\n\\n` +\n `Check your config.server.ts for available configuration paths, or add this path to your base configuration.`\n );\n }\n }\n\n totalValueSize += varValue.length;\n\n envVars.push({\n name: varName,\n path,\n value: varValue,\n depth: path.split('__').length,\n });\n }\n\n if (totalValueSize > MAX_TOTAL_VALUE_SIZE) {\n throw new Error(\n `Total size of PUBLIC__ environment variable values exceeds MRT limit of ${MAX_TOTAL_VALUE_SIZE} bytes (32 KB). ` +\n `Current size: ${totalValueSize} bytes. ` +\n `Consider consolidating configuration using JSON values to reduce the number of variables, ` +\n `or move non-essential configuration to defaults in config.server.ts.`\n );\n }\n\n envVars.sort((a, b) => a.depth - b.depth);\n\n const conflicts: Array<{ parent: string; child: string }> = [];\n for (let i = 0; i < envVars.length; i++) {\n for (let j = i + 1; j < envVars.length; j++) {\n const shorter = envVars[i].path;\n const longer = envVars[j].path;\n if (longer.startsWith(`${shorter}__`)) {\n conflicts.push({\n parent: envVars[i].name,\n child: envVars[j].name,\n });\n }\n }\n }\n\n if (conflicts.length > 0 && process.env.NODE_ENV === 'development') {\n // eslint-disable-next-line no-console\n console.warn(\n `[Config Warning] Conflicting environment variables detected. More specific paths will override parent paths:\\n${conflicts\n .map((c) => ` ${c.parent} ← overridden by → ${c.child}`)\n .join('\\n')}`\n );\n }\n\n let merged: Record<string, unknown> = {};\n\n for (const envVar of envVars) {\n try {\n const parsedValue = parseEnvValue(envVar.value, envVar.name);\n const pathObject = pathToObject(envVar.path, parsedValue, baseConfig);\n merged = deepMerge(merged, pathObject);\n } catch (error) {\n throw new Error(\n `Failed to process environment variable \"${envVar.name}\" with value \"${envVar.value}\": ` +\n `${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n return merged;\n};\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { deepMerge, mergeEnvConfig } from './utils';\n\n/**\n * Base configuration type for storefront-next projects.\n *\n * Generic parameter `App` represents the template's application config shape.\n * The SDK does not prescribe what fields `app` must contain — templates define\n * their own `AppConfig` type with SCAPI credentials, pages, features, etc.\n * and pass it as `BaseConfig<AppConfig>`.\n *\n * The SDK accesses specific `app` fields (e.g., `commerce.api.clientId`) at\n * runtime via the middleware validation, not via compile-time type constraints.\n *\n * @typeParam App - The template's application config shape (defaults to `Record<string, unknown>`)\n *\n * @example\n * // In the template's types file:\n * type AppConfig = { commerce: { api: {...} }; pages: {...}; features: {...} };\n * type Config = BaseConfig<AppConfig>;\n *\n * // In config.server.ts:\n * export default defineConfig<Config>({ metadata: {...}, app: {...} });\n */\nexport type BaseConfig<App extends Record<string, unknown> = Record<string, unknown>> = {\n metadata: {\n projectName: string;\n projectSlug: string;\n };\n runtime?: {\n defaultMrtProject?: string;\n defaultMrtTarget?: string;\n ssrOnly?: string[];\n ssrShared?: string[];\n ssrParameters?: Record<string, string | number | boolean>;\n };\n app: App;\n};\n\nexport interface DefineConfigOptions {\n /**\n * Config paths that cannot be overridden by environment variables.\n * Paths use double underscore separators and are matched case-insensitively.\n *\n * @example ['app__engagement'] — prevents PUBLIC__app__engagement__* from being set via env\n */\n protectedPaths?: string[];\n}\n\n/**\n * Define a type-safe storefront configuration with IDE autocomplete.\n *\n * Automatically merges `PUBLIC__` prefixed environment variables into the config\n * at load time. Validates env vars against the base config structure (strict mode —\n * only allows overriding existing paths).\n *\n * Environment variables:\n * - `PUBLIC__<path>` (optional): Override any config path using double underscore separators.\n * e.g. `PUBLIC__app__commerce__api__clientId=abc123` maps to `config.app.commerce.api.clientId`\n * - `PUBLIC__app__pages__cart__quantityUpdateDebounce=1000` maps to a number (optimistic JSON parsing)\n * - `PUBLIC__app__features__socialLogin__providers=[\"Apple\",\"Google\"]` maps to an array\n *\n * @param config - The base configuration object with all defaults\n * @param options - Optional settings (e.g., protectedPaths to prevent env var overrides)\n * @returns The config with environment variable overrides merged in\n *\n * @example\n * // In config.server.ts:\n * import { defineConfig } from '@salesforce/storefront-next-runtime/config';\n *\n * export default defineConfig({\n * metadata: { projectName: 'My Store', projectSlug: 'my-store' },\n * app: {\n * commerce: { api: { clientId: '', organizationId: '', shortCode: '' }, sites: [] },\n * defaultSiteId: 'RefArch',\n * },\n * }, { protectedPaths: ['app__engagement'] });\n */\nexport function defineConfig<T extends BaseConfig>(config: T, options?: DefineConfigOptions): T {\n const envOverrides = mergeEnvConfig(process.env, config as unknown as Record<string, unknown>, {\n protectedPaths: options?.protectedPaths,\n });\n return deepMerge(config, envOverrides);\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Configuration Context and Provider\n *\n * Provides configuration access throughout the application using React Router's\n * context system. Supports both server and client rendering with proper hydration.\n */\n\nimport { createContext, type ReactNode } from 'react';\nimport { createContext as createRouterContext } from 'react-router';\nimport type { BaseConfig } from './schema';\n\n/**\n * Router context for application configuration.\n *\n * Populated by `createAppConfigMiddleware` with the `app` section of config.\n * Accessible in loaders, actions, and middleware via `context.get(appConfigContext)`.\n */\n// eslint-disable-next-line react-refresh/only-export-components\nexport const appConfigContext = createRouterContext<Record<string, unknown>>();\n\n/**\n * React context for application configuration.\n *\n * Used by the `useConfig()` hook in React components.\n * Populated by `ConfigProvider` in the component tree.\n */\n// eslint-disable-next-line react-refresh/only-export-components\nexport const ConfigContext = createContext<Record<string, unknown> | null>(null);\n\n/**\n * Extract the `app` section from a full config object.\n *\n * @param staticConfig - The full config object (output of `defineConfig()`)\n * @returns The `app` section of the config\n */\n// eslint-disable-next-line react-refresh/only-export-components\nexport function createAppConfig<T extends BaseConfig>(staticConfig: T): T['app'] {\n return staticConfig.app;\n}\n\ninterface ConfigProviderProps {\n config: Record<string, unknown>;\n children: ReactNode;\n}\n\n/**\n * React context provider for application configuration.\n *\n * Wrap your component tree with this to enable `useConfig()` in child components.\n * Typically placed in the root layout component.\n */\nexport function ConfigProvider({ config, children }: ConfigProviderProps) {\n return <ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>;\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Configuration access for loaders, actions, utilities, and React components.\n *\n * Two functions:\n * - `getConfig()` — For loaders, actions, and utilities\n * - `useConfig()` — For React components (hook required for React Context)\n */\n\nimport { useContext } from 'react';\nimport type { RouterContextProvider } from 'react-router';\nimport { ConfigContext, appConfigContext } from './context';\n\ndeclare global {\n interface Window {\n __APP_CONFIG__?: Record<string, unknown>;\n }\n}\n\n/**\n * Get configuration in loaders, actions, and utilities.\n *\n * Pass context parameter in server loaders/actions.\n * Omit context parameter in client loaders (uses window.__APP_CONFIG__).\n *\n * @param context - Router context for server loaders/actions\n * @returns App configuration\n */\nexport function getConfig<T extends Record<string, unknown> = Record<string, unknown>>(\n context?: Readonly<RouterContextProvider>\n): T {\n if (context) {\n const config = context.get(appConfigContext);\n if (!config) {\n throw new Error(\n 'Configuration not available in router context. ' +\n 'Ensure appConfigMiddleware.server runs before other middleware.'\n );\n }\n return config as T;\n }\n\n if (typeof window !== 'undefined' && window.__APP_CONFIG__) {\n return window.__APP_CONFIG__ as T;\n }\n\n throw new Error(\n 'Configuration not available. This can happen if:\\n' +\n '1. Server: Pass context parameter: getConfig(context)\\n' +\n '2. Client: Ensure window.__APP_CONFIG__ was injected during SSR\\n' +\n '3. React component: Use useConfig() hook instead of getConfig()'\n );\n}\n\n/**\n * Get configuration in React components.\n *\n * Must use this hook (not getConfig) because React Context requires useContext().\n *\n * @returns App configuration\n */\nexport function useConfig<T extends Record<string, unknown> = Record<string, unknown>>(): T {\n const config = useContext(ConfigContext);\n if (!config) {\n throw new Error(\n 'useConfig must be used within ConfigProvider. ' +\n 'Ensure ConfigProvider wraps your component tree in root.tsx'\n );\n }\n return config as T;\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { MiddlewareFunction } from 'react-router';\nimport { appConfigContext } from './context';\nimport type { BaseConfig } from './schema';\n\n/**\n * Create app config middleware for both server and client.\n *\n * Follows the same factory pattern as `createMultiSiteMiddleware`.\n *\n * The server middleware:\n * - Validates required Commerce API fields on first request (one-time)\n * - Sets `appConfigContext` in router context with `config.app`\n *\n * The client middleware:\n * - Reads `window.__APP_CONFIG__` (injected during SSR)\n * - Sets `appConfigContext` in router context\n *\n * Environment variables:\n * - `SCAPI_PROXY_HOST` (optional): When set, skips `shortCode` validation\n * (workspace environments route through a proxy that doesn't require shortCode)\n * - `NODE_ENV` (optional): When set to 'test', skips validation entirely\n *\n * @param config - The full config object (output of `defineConfig()`)\n * @returns Object with `server` and `client` middleware functions\n *\n * @example\n * import { createAppConfigMiddleware } from '@salesforce/storefront-next-runtime/config';\n * import config from '@/config/server';\n *\n * const appConfigMiddleware = createAppConfigMiddleware(config);\n *\n * export const middleware = [appConfigMiddleware.server, ...otherMiddleware];\n * export const clientMiddleware = [appConfigMiddleware.client, ...otherClientMiddleware];\n */\nexport function createAppConfigMiddleware<T extends BaseConfig>(\n config: T\n): {\n server: MiddlewareFunction<Response>;\n client: MiddlewareFunction<Record<string, unknown>>;\n} {\n let validationRun = false;\n\n function validateConfig(): void {\n if (validationRun || process.env.NODE_ENV === 'test') {\n return;\n }\n\n const api = (config.app as Record<string, unknown> & { commerce?: { api?: Record<string, string> } }).commerce\n ?.api;\n\n const required: Record<string, string> = {\n clientId: api?.clientId ?? '',\n organizationId: api?.organizationId ?? '',\n };\n\n if (!process.env.SCAPI_PROXY_HOST) {\n required.shortCode = api?.shortCode ?? '';\n }\n\n const missing = Object.entries(required)\n .filter(([_, value]) => !value)\n .map(([key]) => key);\n\n if (missing.length > 0) {\n const envVarMap: Record<string, string> = {\n clientId: 'PUBLIC__app__commerce__api__clientId',\n organizationId: 'PUBLIC__app__commerce__api__organizationId',\n shortCode: 'PUBLIC__app__commerce__api__shortCode',\n };\n\n throw new Error(\n `Missing required Commerce API configuration: ${missing.join(', ')}\\n\\n` +\n `Set these environment variables in your MRT deployment or .env file:\\n${missing\n .map((key) => ` ${envVarMap[key]}=your-value`)\n .join('\\n')}\\n\\n` +\n `Example .env file:\\n` +\n `PUBLIC__app__commerce__api__clientId=your-client-id\\n` +\n `PUBLIC__app__commerce__api__organizationId=your-org-id\\n` +\n `PUBLIC__app__commerce__api__shortCode=your-short-code\\n\\n` +\n `See docs/README-CONFIG.md for complete configuration documentation.`\n );\n }\n\n validationRun = true;\n }\n\n const server: MiddlewareFunction<Response> = ({ context }, next) => {\n validateConfig();\n context.set(appConfigContext, config.app);\n return next();\n };\n\n const client: MiddlewareFunction<Record<string, unknown>> = async ({ context }, next) => {\n const appConfig = typeof window !== 'undefined' ? window.__APP_CONFIG__ : undefined;\n\n if (!appConfig) {\n throw new Error(\n 'window.__APP_CONFIG__ not available. ' +\n 'Check that server loader is injecting config into HTML via Layout component.'\n );\n }\n\n context.set(appConfigContext, appConfig);\n\n return next();\n };\n\n return { server, client };\n}\n"],"mappings":"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"}