@adobe/aio-commerce-lib-app 1.0.2 → 1.1.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 (54) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cjs/actions/app-config.cjs +18 -4
  3. package/dist/cjs/actions/app-config.d.cts +15 -1
  4. package/dist/cjs/actions/config.cjs +17 -3
  5. package/dist/cjs/actions/config.d.cts +14 -0
  6. package/dist/cjs/actions/installation.cjs +70 -16
  7. package/dist/cjs/actions/installation.d.cts +16 -2
  8. package/dist/cjs/actions/scope-tree.cjs +17 -3
  9. package/dist/cjs/actions/scope-tree.d.cts +14 -0
  10. package/dist/cjs/{app-DWX5-Hsf.d.cts → app-lymFcs59.d.cts} +146 -1
  11. package/dist/cjs/commands/index.cjs +41 -27
  12. package/dist/cjs/commands/index.d.cts +14 -0
  13. package/dist/cjs/config/index.cjs +27 -12
  14. package/dist/cjs/config/index.d.cts +576 -4
  15. package/dist/cjs/{parser-BPpg_9QB.cjs → config-YEeaEqzi.cjs} +17 -3
  16. package/dist/cjs/error-Dn7ool6k.cjs +38 -0
  17. package/dist/{es/runner-BD-lItnK.d.mts → cjs/index-DRhLtRrX.d.cts} +102 -4
  18. package/dist/cjs/logging-4s36JTiN.cjs +39 -0
  19. package/dist/cjs/management/index.cjs +23 -8
  20. package/dist/cjs/management/index.d.cts +16 -2
  21. package/dist/cjs/{runner-NHMvoMO2.cjs → management-PZtLe4Ji.cjs} +371 -30
  22. package/dist/cjs/{router-DCw7oEQ9.cjs → router-KeQRduO3.cjs} +15 -1
  23. package/dist/cjs/{schemas-CZ6c8Id9.cjs → schemas-nkIxa8sL.cjs} +34 -0
  24. package/dist/cjs/{validate-Btzn9ilZ.cjs → validate-CwwYD8aC.cjs} +31 -14
  25. package/dist/cjs/{installation-CLbceU9F.cjs → webhooks-CbZpv9y_.cjs} +105 -1
  26. package/dist/es/actions/app-config.d.mts +15 -1
  27. package/dist/es/actions/app-config.mjs +17 -3
  28. package/dist/es/actions/config.d.mts +14 -0
  29. package/dist/es/actions/config.mjs +17 -3
  30. package/dist/es/actions/installation.d.mts +16 -2
  31. package/dist/es/actions/installation.mjs +64 -10
  32. package/dist/es/actions/scope-tree.d.mts +14 -0
  33. package/dist/es/actions/scope-tree.mjs +17 -3
  34. package/dist/es/{app-BAiyvNo2.d.mts → app-Ct7Y0NP8.d.mts} +146 -1
  35. package/dist/es/commands/index.d.mts +14 -0
  36. package/dist/es/commands/index.mjs +21 -7
  37. package/dist/es/config/index.d.mts +576 -4
  38. package/dist/es/config/index.mjs +18 -4
  39. package/dist/es/{parser-CQZTVG6i.mjs → config-BbrkH0Xt.mjs} +16 -2
  40. package/dist/es/error-DHlYzkbb.mjs +32 -0
  41. package/dist/{cjs/runner-DemKouFJ.d.cts → es/index-D33OCH0D.d.mts} +102 -4
  42. package/dist/es/logging-XIUXDK5T.mjs +32 -0
  43. package/dist/es/management/index.d.mts +16 -2
  44. package/dist/es/management/index.mjs +16 -2
  45. package/dist/es/{runner-vwAhjD5r.mjs → management-CIoVWirU.mjs} +360 -25
  46. package/dist/es/{router-CJ4VWoCt.mjs → router-BxaxEEu3.mjs} +14 -0
  47. package/dist/es/{schemas-B8yIv0_b.mjs → schemas-BvPxQwgQ.mjs} +29 -1
  48. package/dist/es/{validate-DKnju9-R.mjs → validate-qRpfubPo.mjs} +21 -4
  49. package/dist/es/{installation-BTL9X7iv.mjs → webhooks-NgM6k3_r.mjs} +94 -2
  50. package/package.json +10 -7
  51. package/dist/cjs/error-DJ2UAPH2.cjs +0 -24
  52. package/dist/cjs/logging-IDRQG0as.cjs +0 -25
  53. package/dist/es/error-CMV3IjBz.mjs +0 -18
  54. package/dist/es/logging-CzmXDzxI.mjs +0 -18
@@ -1,11 +1,27 @@
1
- import { a as hasCommerceEvents, o as hasEventing, r as hasCustomInstallationSteps, s as hasExternalEvents } from "./installation-BTL9X7iv.mjs";
2
- import { t as stringifyError } from "./error-CMV3IjBz.mjs";
3
- import { t as inspect } from "./logging-CzmXDzxI.mjs";
1
+ /**
2
+ * @license
3
+ *
4
+ * Copyright 2026 Adobe. All rights reserved.
5
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License. You may obtain a copy
7
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software distributed under
10
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
11
+ * OF ANY KIND, either express or implied. See the License for the specific language
12
+ * governing permissions and limitations under the License.
13
+ */
14
+
15
+ import { a as hasCustomInstallationSteps, c as hasEventing, l as hasExternalEvents, n as hasWebhooks, s as hasCommerceEvents } from "./webhooks-NgM6k3_r.mjs";
16
+ import { t as stringifyError } from "./error-DHlYzkbb.mjs";
17
+ import { t as inspect } from "./logging-XIUXDK5T.mjs";
4
18
  import camelcase from "camelcase";
5
- import { resolveAuthParams } from "@adobe/aio-commerce-lib-auth";
19
+ import { resolveAuthParams, resolveImsAuthParams } from "@adobe/aio-commerce-lib-auth";
6
20
  import { resolveCommerceHttpClientParams, resolveIoEventsHttpClientParams } from "@adobe/aio-commerce-lib-api";
7
21
  import { createCustomCommerceEventsApiClient, createEventProvider, createEventSubscription, getAllEventProviders, getAllEventSubscriptions, updateEventingConfiguration } from "@adobe/aio-commerce-lib-events/commerce";
8
22
  import { createCustomAdobeIoEventsApiClient, createEventMetadataForProvider, createEventProvider as createEventProvider$1, createRegistration, getAllEventProviders as getAllEventProviders$1, getAllRegistrations } from "@adobe/aio-commerce-lib-events/io-events";
23
+ import { createCustomCommerceWebhooksApiClient, getWebhookList, subscribeWebhook } from "@adobe/aio-commerce-lib-webhooks/api";
24
+ import { HTTPError } from "ky";
9
25
 
10
26
  //#region source/management/installation/workflow/hooks.ts
11
27
  /** Helper to call a hook if it exists. */
@@ -45,7 +61,8 @@ function defineLeafStep(options) {
45
61
  name: options.name,
46
62
  meta: options.meta,
47
63
  when: options.when,
48
- run: options.run
64
+ run: options.run,
65
+ validate: options.validate
49
66
  };
50
67
  }
51
68
  /**
@@ -69,7 +86,8 @@ function defineBranchStep(options) {
69
86
  meta: options.meta,
70
87
  when: options.when,
71
88
  context: options.context,
72
- children: options.children
89
+ children: options.children,
90
+ validate: options.validate
73
91
  };
74
92
  }
75
93
 
@@ -293,6 +311,104 @@ function isCompletedState(state) {
293
311
  return state.status === "succeeded" || state.status === "failed";
294
312
  }
295
313
 
314
+ //#endregion
315
+ //#region source/management/installation/workflow/validation.ts
316
+ /**
317
+ * Runs validation over the full step tree, returning a structured result.
318
+ *
319
+ * - Respects `when` conditions (skips steps that don't apply to the config)
320
+ * - Calls each step's optional `validate` handler
321
+ * - Sets up branch context factories before validating children
322
+ * - Never throws; all errors from validate handlers are caught and reported as issues
323
+ */
324
+ async function validateStepTree(options) {
325
+ const { rootStep, validationContext, config } = options;
326
+ const result = await validateStep(rootStep, config, validationContext, []);
327
+ const summary = aggregateSummary(result);
328
+ return {
329
+ valid: summary.errors === 0 && summary.warnings === 0,
330
+ result,
331
+ summary
332
+ };
333
+ }
334
+ /** Recursively validates a single step and its children. */
335
+ async function validateStep(step, config, context, parentPath) {
336
+ const path = [...parentPath, step.name];
337
+ const issues = await runStepValidation(step, config, context);
338
+ const children = [];
339
+ if (isBranchStep(step) && step.children.length > 0) {
340
+ const resolved = await resolveBranchContext(step, context);
341
+ issues.push(...resolved.issues);
342
+ for (const child of step.children) {
343
+ if (child.when && !child.when(config)) continue;
344
+ children.push(await validateStep(child, config, resolved.childContext, path));
345
+ }
346
+ }
347
+ return {
348
+ name: step.name,
349
+ path,
350
+ meta: step.meta,
351
+ issues,
352
+ children
353
+ };
354
+ }
355
+ /** Resolves the child context for a branch step, reporting errors as issues. */
356
+ async function resolveBranchContext(step, context) {
357
+ if (!step.context) return {
358
+ childContext: context,
359
+ issues: []
360
+ };
361
+ try {
362
+ const stepContext = await step.context(context);
363
+ return {
364
+ childContext: {
365
+ ...context,
366
+ ...stepContext
367
+ },
368
+ issues: []
369
+ };
370
+ } catch (err) {
371
+ return {
372
+ childContext: context,
373
+ issues: [{
374
+ code: "VALIDATION_CONTEXT_ERROR",
375
+ message: err instanceof Error ? err.message : String(err),
376
+ severity: "error"
377
+ }]
378
+ };
379
+ }
380
+ }
381
+ /** Runs a step's validate handler, catching any thrown errors as issues. */
382
+ async function runStepValidation(step, config, context) {
383
+ if (!step.validate) return [];
384
+ try {
385
+ return await step.validate(config, context);
386
+ } catch (err) {
387
+ return [{
388
+ code: "VALIDATION_HANDLER_ERROR",
389
+ message: err instanceof Error ? err.message : String(err),
390
+ severity: "error"
391
+ }];
392
+ }
393
+ }
394
+ /** Recursively aggregates issue counts across the full validation tree. */
395
+ function aggregateSummary(result) {
396
+ let errors = 0;
397
+ let warnings = 0;
398
+ for (const issue of result.issues) if (issue.severity === "error") errors++;
399
+ else if (issue.severity === "warning") warnings++;
400
+ for (const child of result.children) {
401
+ const childSummary = aggregateSummary(child);
402
+ errors += childSummary.errors;
403
+ warnings += childSummary.warnings;
404
+ }
405
+ return {
406
+ totalIssues: errors + warnings,
407
+ errors,
408
+ warnings
409
+ };
410
+ }
411
+
296
412
  //#endregion
297
413
  //#region source/management/installation/custom-installation/custom-scripts.ts
298
414
  /**
@@ -408,7 +524,7 @@ const PROVIDER_TYPE_TO_LABEL = {
408
524
  */
409
525
  function generateInstanceId(metadata, provider) {
410
526
  const slugLabel = provider.label.toLowerCase().replace(/\s+/g, "-");
411
- return `${metadata.id}-${provider.key ?? slugLabel}`;
527
+ return `${metadata.id}-${provider.key ?? slugLabel}`.toLowerCase();
412
528
  }
413
529
  /**
414
530
  * Find an existing event provider by its instance ID.
@@ -442,7 +558,7 @@ function findExistingRegistrations(allRegistrations, clientId, name) {
442
558
  * @param name
443
559
  */
444
560
  function getNamespacedEvent(metadata, name) {
445
- return `${metadata.id}.${name}`;
561
+ return `${metadata.id}.${name}`.toLowerCase();
446
562
  }
447
563
  /**
448
564
  * Get the fully qualified name of an event for I/O Events based on the provider type.
@@ -450,7 +566,7 @@ function getNamespacedEvent(metadata, name) {
450
566
  * @param providerType - The type of the event provider.
451
567
  */
452
568
  function getIoEventCode(name, providerType) {
453
- return providerType === COMMERCE_PROVIDER_TYPE ? `com.adobe.commerce.${name}` : name;
569
+ return providerType === "dx_commerce_events" ? `com.adobe.commerce.${name}` : name;
454
570
  }
455
571
  /**
456
572
  * Generates a registration name and description based on the provider, events, and runtime action.
@@ -1099,19 +1215,224 @@ const eventingStep = defineBranchStep({
1099
1215
  });
1100
1216
 
1101
1217
  //#endregion
1102
- //#region source/management/installation/webhooks/helpers.ts
1103
- function createWebhookSubscriptions(context) {
1104
- const { logger } = context;
1105
- logger.info("Creating webhooks in Commerce");
1106
- return { subscriptionsCreated: true };
1218
+ //#region source/management/installation/webhooks/context.ts
1219
+ /**
1220
+ * Create a custom Commerce Webhooks API Client with only the operations needed for installation.
1221
+ * @param params - The runtime action params to resolve the client params from.
1222
+ */
1223
+ function createCommerceWebhooksApiClient(params) {
1224
+ const commerceClientParams = resolveCommerceHttpClientParams(params, { tryForwardAuthProvider: true });
1225
+ commerceClientParams.fetchOptions ??= {};
1226
+ commerceClientParams.fetchOptions.timeout = 1e3 * 60 * 2;
1227
+ return createCustomCommerceWebhooksApiClient(commerceClientParams, {
1228
+ getWebhookList,
1229
+ subscribeWebhook
1230
+ });
1231
+ }
1232
+ /** Creates the webhooks step context with a lazy-initialized API client. */
1233
+ function createWebhooksStepContext(installation) {
1234
+ const { params } = installation;
1235
+ let commerceWebhooksClient = null;
1236
+ return { get commerceWebhooksClient() {
1237
+ if (commerceWebhooksClient === null) commerceWebhooksClient = createCommerceWebhooksApiClient(params);
1238
+ return commerceWebhooksClient;
1239
+ } };
1107
1240
  }
1108
1241
 
1109
1242
  //#endregion
1110
- //#region source/management/installation/webhooks/utils.ts
1111
- /** Check if config has webhooks. */
1112
- function hasWebhooks(config) {
1113
- "webhooks" in config && Array.isArray(config.webhooks) && config.webhooks.length;
1114
- return false;
1243
+ //#region source/management/installation/webhooks/helpers.ts
1244
+ /** Matches any character that is not a valid identifier character (letter, digit, or underscore). */
1245
+ const NON_IDENTIFIER_CHAR_REGEX = /[^a-zA-Z0-9_]/g;
1246
+ /** Matches two or more consecutive underscores. */
1247
+ const MULTIPLE_UNDERSCORES_REGEX = /_+/g;
1248
+ /** Matches the `.magento` segment in plugin webhook method names (e.g. `plugin.magento.foo`). */
1249
+ const PLUGIN_MAGENTO_REGEX = /^plugin\.magento\./;
1250
+ const ENVIRONMENT_PRODUCTION = "production";
1251
+ const ENVIRONMENT_STAGING = "staging";
1252
+ /**
1253
+ * Validates that no modification webhooks in the app config conflict with webhooks
1254
+ * already registered in Commerce by another app.
1255
+ *
1256
+ * A conflict is: Commerce has a webhook with the same `webhook_method` and `webhook_type`
1257
+ * that does NOT belong to this app (i.e. different `batch_name` or `hook_name` after prefix).
1258
+ *
1259
+ * Returns a `ValidationIssue` with code `WEBHOOK_CONFLICTS` and `details.conflictedWebhooks` listing
1260
+ * every conflicting Commerce webhook when conflicts are found, or an empty array otherwise.
1261
+ *
1262
+ * @param config - The app config (must have a non-empty `webhooks` array).
1263
+ * @param context - The webhooks execution context (provides the Commerce API client and logger).
1264
+ */
1265
+ async function validateWebhookConflicts(config, context) {
1266
+ const { logger, commerceWebhooksClient } = context;
1267
+ const modificationWebhooks = config.webhooks.filter((entry) => entry.category === "modification");
1268
+ if (modificationWebhooks.length === 0) {
1269
+ logger.debug("No modification webhooks to validate, skipping conflict check.");
1270
+ return [];
1271
+ }
1272
+ logger.debug(`Validating ${modificationWebhooks.length} modification webhook(s) for conflicts...`);
1273
+ const existingWebhooks = await commerceWebhooksClient.getWebhookList();
1274
+ const idPrefix = buildWebhookIdPrefix(config.metadata.id);
1275
+ const conflictedWebhooks = [];
1276
+ for (const entry of modificationWebhooks) {
1277
+ const { webhook } = entry;
1278
+ const resolvedBatch = `${idPrefix}${webhook.batch_name}`;
1279
+ const resolvedHook = `${idPrefix}${webhook.hook_name}`;
1280
+ for (const existing of existingWebhooks) if (existing.webhook_method === webhook.webhook_method && existing.webhook_type === webhook.webhook_type && !(existing.batch_name === resolvedBatch && existing.hook_name === resolvedHook)) {
1281
+ conflictedWebhooks.push({
1282
+ label: entry.label,
1283
+ ...existing
1284
+ });
1285
+ break;
1286
+ }
1287
+ }
1288
+ if (conflictedWebhooks.length > 0) return [{
1289
+ code: "WEBHOOK_CONFLICTS",
1290
+ message: `Webhook conflicts detected: ${conflictedWebhooks.length} webhook(s) already registered for the same method and type by another app`,
1291
+ severity: "warning",
1292
+ details: { conflictedWebhooks }
1293
+ }];
1294
+ logger.info("No webhook conflicts found.");
1295
+ return [];
1296
+ }
1297
+ /**
1298
+ * Subscribes each webhook from the app config to Adobe Commerce.
1299
+ * Throws on the first failure, aborting any remaining subscriptions.
1300
+ *
1301
+ * @param config - The app config (must have a non-empty `webhooks` array).
1302
+ * @param context - The webhooks execution context (provides the Commerce API client and logger).
1303
+ */
1304
+ async function createWebhookSubscriptions(config, context) {
1305
+ const { logger, commerceWebhooksClient, params } = context;
1306
+ logger.info(`Subscribing ${config.webhooks.length} webhook(s) to Commerce...`);
1307
+ const idPrefix = buildWebhookIdPrefix(config.metadata.id);
1308
+ const subscribedWebhooks = [];
1309
+ const existingWebhooks = await commerceWebhooksClient.getWebhookList();
1310
+ for (const entry of config.webhooks) {
1311
+ const { webhook } = entry;
1312
+ const resolvedUrl = "runtimeAction" in entry ? generateUrlForRuntimeAction(entry.runtimeAction) : entry.webhook.url;
1313
+ logger.debug(`Subscribing webhook "${getWebhookName(webhook)}" (runtimeAction: ${"runtimeAction" in entry ? entry.runtimeAction : "none"})`);
1314
+ const resolvedWebhook = {
1315
+ ...webhook,
1316
+ url: resolvedUrl,
1317
+ batch_name: `${idPrefix}${webhook.batch_name}`,
1318
+ hook_name: `${idPrefix}${webhook.hook_name}`,
1319
+ ..."runtimeAction" in entry && entry.requireAdobeAuth !== false && { developer_console_oauth: resolveDeveloperConsoleOAuthCredentials(params) }
1320
+ };
1321
+ subscribedWebhooks.push(await createOrGetWebhookSubscription(existingWebhooks, commerceWebhooksClient, resolvedWebhook, logger));
1322
+ }
1323
+ logger.info(`Webhook subscriptions complete: ${subscribedWebhooks.length} subscribed.`);
1324
+ return { subscribedWebhooks };
1325
+ }
1326
+ /**
1327
+ * Subscribes a single webhook to Commerce, skipping the API call if the webhook
1328
+ * is already subscribed (matched by webhook_method, webhook_type, batch_name, hook_name).
1329
+ */
1330
+ async function createOrGetWebhookSubscription(existingWebhooks, client, resolvedWebhook, logger) {
1331
+ if (isAlreadySubscribed(existingWebhooks, resolvedWebhook)) {
1332
+ logger.info(`Webhook already subscribed, skipping: ${getWebhookName(resolvedWebhook)}`);
1333
+ return resolvedWebhook;
1334
+ }
1335
+ const subscribed = await createWebhookSubscription(client, resolvedWebhook);
1336
+ logger.info(`Subscribed webhook: ${getWebhookName(resolvedWebhook)}`);
1337
+ return subscribed;
1338
+ }
1339
+ /**
1340
+ * Subscribes a single webhook to Commerce, enriching the error with the webhook name
1341
+ * if the API responds with a string `message`.
1342
+ */
1343
+ async function createWebhookSubscription(client, resolvedWebhook) {
1344
+ try {
1345
+ await client.subscribeWebhook(resolvedWebhook);
1346
+ return resolvedWebhook;
1347
+ } catch (err) {
1348
+ if (err instanceof HTTPError) {
1349
+ let body;
1350
+ try {
1351
+ body = await err.response.json();
1352
+ } catch {
1353
+ throw err;
1354
+ }
1355
+ if (typeof body?.message === "string") throw new Error(`Webhook subscription failed for "${getWebhookName(resolvedWebhook)}": ${body.message}`);
1356
+ }
1357
+ throw err;
1358
+ }
1359
+ }
1360
+ /**
1361
+ * Resolves and validates the IMS credentials required for `developer_console_oauth`.
1362
+ *
1363
+ * Delegates parsing and validation to `resolveImsAuthParams` from `aio-commerce-lib-auth`,
1364
+ * which correctly handles `AIO_COMMERCE_AUTH_IMS_CLIENT_SECRETS` whether it arrives as a
1365
+ * real array or as a JSON-stringified array string.
1366
+ */
1367
+ function resolveDeveloperConsoleOAuthCredentials(params) {
1368
+ const { AIO_COMMERCE_AUTH_IMS_ENVIRONMENT: imsEnvironment, ...imsParams } = params;
1369
+ const { clientId, clientSecrets, imsOrgId } = resolveImsAuthParams(imsParams);
1370
+ return {
1371
+ client_id: clientId,
1372
+ client_secret: clientSecrets[0],
1373
+ org_id: imsOrgId,
1374
+ environment: !imsEnvironment || String(imsEnvironment).startsWith("prod") ? ENVIRONMENT_PRODUCTION : ENVIRONMENT_STAGING
1375
+ };
1376
+ }
1377
+ /**
1378
+ * Returns true when the candidate webhook is already present in the existing subscription list,
1379
+ * matched by the four-part identity: webhook_method, webhook_type, batch_name, hook_name.
1380
+ *
1381
+ * `webhook_method` is normalised before comparison to handle the case where Commerce strips the
1382
+ * `.magento` segment from plugin webhook methods on storage
1383
+ * (e.g. `plugin.magento.foo` and `plugin.foo` are treated as the same method).
1384
+ */
1385
+ function isAlreadySubscribed(existing, candidate) {
1386
+ const normalizedCandidate = normalizeWebhookMethod(candidate.webhook_method);
1387
+ return existing.some((w) => normalizeWebhookMethod(w.webhook_method) === normalizedCandidate && w.webhook_type === candidate.webhook_type && w.batch_name === candidate.batch_name && w.hook_name === candidate.hook_name);
1388
+ }
1389
+ /**
1390
+ * Normalises a webhook method name by removing the `.magento` segment that Commerce
1391
+ * may drop when persisting plugin webhook methods.
1392
+ *
1393
+ * @example
1394
+ * normalizeWebhookMethod("plugin.magento.foo.bar") // → "plugin.foo.bar"
1395
+ * normalizeWebhookMethod("plugin.foo.bar") // → "plugin.foo.bar" (unchanged)
1396
+ */
1397
+ function normalizeWebhookMethod(method) {
1398
+ return method.replace(PLUGIN_MAGENTO_REGEX, "plugin.");
1399
+ }
1400
+ /**
1401
+ * Generates a URL for a given runtime action using the AIO Runtime API host and namespace.
1402
+ * @param runtimeAction
1403
+ * @return The generated URL for the runtime action.
1404
+ */
1405
+ function generateUrlForRuntimeAction(runtimeAction) {
1406
+ const namespace = process.env.__OW_NAMESPACE;
1407
+ if (!namespace) throw new Error(`Cannot generate URL for runtime action "${runtimeAction}": namespace environment variable is not set.`);
1408
+ return `https://${namespace}.adobeioruntime.net/api/v1/web/${runtimeAction}`;
1409
+ }
1410
+ /**
1411
+ * Builds a prefix string from the app ID to namespace webhook batch/hook names.
1412
+ * Non-identifier characters are replaced with underscores; consecutive underscores
1413
+ * are collapsed to one; a trailing underscore is appended. The result is lowercased
1414
+ * to ensure consistent matching regardless of input casing.
1415
+ *
1416
+ * @example
1417
+ * ```typescript
1418
+ * buildWebhookIdPrefix("my--app.v2") // => "my_app_v2_"
1419
+ * buildWebhookIdPrefix("MyApp") // => "myapp_"
1420
+ * ```
1421
+ * @param appId - The app ID to build the prefix from.
1422
+ * @return The built prefix string.
1423
+ */
1424
+ function buildWebhookIdPrefix(appId) {
1425
+ const prefix = appId.toLowerCase().replace(NON_IDENTIFIER_CHAR_REGEX, "_").replace(MULTIPLE_UNDERSCORES_REGEX, "_");
1426
+ return prefix.endsWith("_") ? prefix : `${prefix}_`;
1427
+ }
1428
+ /**
1429
+ * Generates a name for a webhook based on its method and type.
1430
+ *
1431
+ * @param webhook
1432
+ * @return A string in the format "webhook_method:webhook_type" to identify the webhook.
1433
+ */
1434
+ function getWebhookName(webhook) {
1435
+ return `${webhook.webhook_method}:${webhook.webhook_type}`;
1115
1436
  }
1116
1437
 
1117
1438
  //#endregion
@@ -1122,11 +1443,8 @@ const subscriptionsStep = defineLeafStep({
1122
1443
  label: "Create Subscriptions",
1123
1444
  description: "Creates webhook subscriptions in Adobe Commerce"
1124
1445
  },
1125
- run: (config, context) => {
1126
- const { logger } = context;
1127
- logger.debug(config);
1128
- return createWebhookSubscriptions(context);
1129
- }
1446
+ validate: (config, context) => validateWebhookConflicts(config, context),
1447
+ run: (config, context) => createWebhookSubscriptions(config, context)
1130
1448
  });
1131
1449
  /** Branch step for setting up Commerce webhooks. */
1132
1450
  const webhooksStep = defineBranchStep({
@@ -1136,6 +1454,7 @@ const webhooksStep = defineBranchStep({
1136
1454
  description: "Sets up Commerce webhooks"
1137
1455
  },
1138
1456
  when: hasWebhooks,
1457
+ context: createWebhooksStepContext,
1139
1458
  children: [subscriptionsStep]
1140
1459
  });
1141
1460
 
@@ -1192,6 +1511,22 @@ function runInstallation(options) {
1192
1511
  hooks
1193
1512
  });
1194
1513
  }
1514
+ /**
1515
+ * Runs pre-installation validation over the full step tree.
1516
+ *
1517
+ * Traverses the same step hierarchy used during installation but only calls
1518
+ * each step's optional `validate` handler rather than executing side effects.
1519
+ * Always resolves (never throws). Returns a structured result with per-step
1520
+ * issues and an aggregated summary.
1521
+ */
1522
+ function runValidation(options) {
1523
+ const { validationContext, config } = options;
1524
+ return validateStepTree({
1525
+ rootStep: createRootInstallationStep(config),
1526
+ validationContext,
1527
+ config
1528
+ });
1529
+ }
1195
1530
 
1196
1531
  //#endregion
1197
- export { isFailedState as a, isCompletedState as i, runInstallation as n, isInProgressState as o, defineCustomInstallationStep as r, isSucceededState as s, createInitialInstallationState as t };
1532
+ export { isCompletedState as a, isSucceededState as c, defineCustomInstallationStep as i, runInstallation as n, isFailedState as o, runValidation as r, isInProgressState as s, createInitialInstallationState as t };
@@ -1,3 +1,17 @@
1
+ /**
2
+ * @license
3
+ *
4
+ * Copyright 2026 Adobe. All rights reserved.
5
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License. You may obtain a copy
7
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software distributed under
10
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
11
+ * OF ANY KIND, either express or implied. See the License for the specific language
12
+ * governing permissions and limitations under the License.
13
+ */
14
+
1
15
  import { badRequest, internalServerError, methodNotAllowed, notFound } from "@adobe/aio-commerce-lib-core/responses";
2
16
  import AioLogger from "@adobe/aio-lib-core-logging";
3
17
  import { parse } from "regexparam";
@@ -1,3 +1,17 @@
1
+ /**
2
+ * @license
3
+ *
4
+ * Copyright 2026 Adobe. All rights reserved.
5
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License. You may obtain a copy
7
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software distributed under
10
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
11
+ * OF ANY KIND, either express or implied. See the License for the specific language
12
+ * governing permissions and limitations under the License.
13
+ */
14
+
1
15
  import * as v from "valibot";
2
16
 
3
17
  //#region ../../packages-private/common-utils/source/valibot/schemas.ts
@@ -7,6 +21,20 @@ const ALPHANUMERIC_OR_HYPHEN_REGEX = {
7
21
  uppercase: /^[A-Z0-9-]+$/
8
22
  };
9
23
  /**
24
+ * A schema for a number value.
25
+ * @param name The name of the field this schema refers to.
26
+ */
27
+ function numberValueSchema(name) {
28
+ return v.number(`Expected a number value for '${name}'`);
29
+ }
30
+ /**
31
+ * A schema for a positive number value (including zero).
32
+ * @param name The name of the field this schema refers to.
33
+ */
34
+ function positiveNumberValueSchema(name) {
35
+ return v.pipe(numberValueSchema(name), v.minValue(0, `The value of ${name} must be a non-negative number`));
36
+ }
37
+ /**
10
38
  * A schema for a string value.
11
39
  * @param name The name of the field this schema refers to.
12
40
  */
@@ -38,4 +66,4 @@ function alphaNumericOrHyphenSchema(name, casing = "any") {
38
66
  }
39
67
 
40
68
  //#endregion
41
- export { stringValueSchema as i, booleanValueSchema as n, nonEmptyStringValueSchema as r, alphaNumericOrHyphenSchema as t };
69
+ export { stringValueSchema as a, positiveNumberValueSchema as i, booleanValueSchema as n, nonEmptyStringValueSchema as r, alphaNumericOrHyphenSchema as t };
@@ -1,5 +1,19 @@
1
- import { t as alphaNumericOrHyphenSchema } from "./schemas-B8yIv0_b.mjs";
2
- import { a as hasCommerceEvents, i as EventingSchema, n as hasCustomInstallation, r as hasCustomInstallationSteps, s as hasExternalEvents, t as InstallationSchema } from "./installation-BTL9X7iv.mjs";
1
+ /**
2
+ * @license
3
+ *
4
+ * Copyright 2026 Adobe. All rights reserved.
5
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License. You may obtain a copy
7
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software distributed under
10
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
11
+ * OF ANY KIND, either express or implied. See the License for the specific language
12
+ * governing permissions and limitations under the License.
13
+ */
14
+
15
+ import { t as alphaNumericOrHyphenSchema } from "./schemas-BvPxQwgQ.mjs";
16
+ import { a as hasCustomInstallationSteps, i as hasCustomInstallation, l as hasExternalEvents, n as hasWebhooks, o as EventingSchema, r as InstallationSchema, s as hasCommerceEvents, t as WebhooksSchema } from "./webhooks-NgM6k3_r.mjs";
3
17
  import { CommerceSdkValidationError } from "@adobe/aio-commerce-lib-core/error";
4
18
  import * as v from "valibot";
5
19
  import { SchemaBusinessConfig } from "@adobe/aio-commerce-lib-config";
@@ -34,7 +48,7 @@ const MetadataSchema = v.object({
34
48
  id: alphaNumericOrHyphenSchema("application id (metadata.id)"),
35
49
  displayName: v.pipe(nonEmptyString("application display name"), v.maxLength(MAX_DISPLAY_NAME_LENGTH, `The application display name must not be longer than ${MAX_DISPLAY_NAME_LENGTH} characters`)),
36
50
  description: v.pipe(nonEmptyString("metadata description"), v.maxLength(MAX_DESCRIPTION_LENGTH, `The metadata description must not be longer than ${MAX_DESCRIPTION_LENGTH} characters`)),
37
- version: v.pipe(nonEmptyString("version"), v.regex(SEMVER_REGEX, "The version must follow semantic versioning (semver) format"))
51
+ version: v.pipe(nonEmptyString("version"), v.regex(SEMVER_REGEX, "The version must follow semantic versioning (semver) format: Major.Minor.Patch (e.g., '1.0.0', '2.3.1')"))
38
52
  });
39
53
  /**
40
54
  * Check if config has metadata.
@@ -52,7 +66,8 @@ const CommerceAppConfigSchema = v.looseObject({
52
66
  metadata: MetadataSchema,
53
67
  businessConfig: v.optional(SchemaBusinessConfig),
54
68
  eventing: v.optional(EventingSchema),
55
- installation: v.optional(InstallationSchema)
69
+ installation: v.optional(InstallationSchema),
70
+ webhooks: v.optional(WebhooksSchema)
56
71
  });
57
72
 
58
73
  //#endregion
@@ -63,6 +78,7 @@ const CommerceAppConfigSchemas = {
63
78
  businessConfig: SchemaBusinessConfig,
64
79
  eventing: EventingSchema,
65
80
  installation: InstallationSchema,
81
+ webhooks: WebhooksSchema,
66
82
  "businessConfig.schema": v.unwrap(SchemaBusinessConfig.entries.schema),
67
83
  "eventing.commerce": v.unwrap(EventingSchema.entries.commerce),
68
84
  "eventing.external": v.unwrap(EventingSchema.entries.external),
@@ -80,6 +96,7 @@ function getConfigDomains(config) {
80
96
  businessConfig: hasBusinessConfig(config),
81
97
  eventing: withCommerceEvents || withExternalEvents,
82
98
  installation: hasCustomInstallation(config),
99
+ webhooks: hasWebhooks(config),
83
100
  "businessConfig.schema": hasBusinessConfigSchema(config),
84
101
  "eventing.commerce": withCommerceEvents,
85
102
  "eventing.external": withExternalEvents,