@adobe/aio-commerce-lib-app 1.1.0 → 1.3.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 (40) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/cjs/actions/app-config.cjs +1 -1
  3. package/dist/cjs/actions/app-config.d.cts +3 -3
  4. package/dist/cjs/actions/config.cjs +33 -2
  5. package/dist/cjs/actions/config.d.cts +2 -2
  6. package/dist/cjs/actions/installation.cjs +167 -43
  7. package/dist/cjs/actions/installation.d.cts +4 -4
  8. package/dist/cjs/actions/scope-tree.d.cts +3 -3
  9. package/dist/cjs/{app-lymFcs59.d.cts → app-DcQMhW2N.d.cts} +13 -1
  10. package/dist/cjs/commands/index.cjs +3 -3
  11. package/dist/cjs/config/index.cjs +3 -3
  12. package/dist/cjs/config/index.d.cts +39 -3
  13. package/dist/cjs/{config-YEeaEqzi.cjs → config-BppBKCFj.cjs} +1 -1
  14. package/dist/{es/index-D33OCH0D.d.mts → cjs/index-BVwQk9bx.d.cts} +103 -34
  15. package/dist/cjs/management/index.cjs +3 -1
  16. package/dist/cjs/management/index.d.cts +2 -2
  17. package/dist/cjs/{management-PZtLe4Ji.cjs → management-C6xG5bfl.cjs} +731 -163
  18. package/dist/cjs/{validate-CwwYD8aC.cjs → validate-CeUCT_7k.cjs} +7 -3
  19. package/dist/cjs/{webhooks-CbZpv9y_.cjs → webhooks-CLtDxwMa.cjs} +33 -0
  20. package/dist/es/actions/app-config.d.mts +3 -3
  21. package/dist/es/actions/app-config.mjs +1 -1
  22. package/dist/es/actions/config.d.mts +2 -2
  23. package/dist/es/actions/config.mjs +34 -3
  24. package/dist/es/actions/installation.d.mts +4 -4
  25. package/dist/es/actions/installation.mjs +168 -44
  26. package/dist/es/actions/scope-tree.d.mts +3 -3
  27. package/dist/es/actions/scope-tree.mjs +1 -1
  28. package/dist/es/{app-Ct7Y0NP8.d.mts → app-DJr-mN9d.d.mts} +13 -1
  29. package/dist/es/commands/index.mjs +3 -3
  30. package/dist/es/config/index.d.mts +39 -3
  31. package/dist/es/config/index.mjs +3 -3
  32. package/dist/es/{config-BbrkH0Xt.mjs → config-BohwKkQS.mjs} +1 -1
  33. package/dist/{cjs/index-DRhLtRrX.d.cts → es/index-BENO5T7n.d.mts} +103 -34
  34. package/dist/es/management/index.d.mts +2 -2
  35. package/dist/es/management/index.mjs +2 -2
  36. package/dist/es/{management-CIoVWirU.mjs → management-ByHvVJ12.mjs} +723 -167
  37. package/dist/es/{validate-qRpfubPo.mjs → validate-CqJdGzyZ.mjs} +8 -4
  38. package/dist/es/{webhooks-NgM6k3_r.mjs → webhooks-CYo-pqbR.mjs} +23 -2
  39. package/package.json +5 -4
  40. /package/dist/es/{schemas-BvPxQwgQ.mjs → schemas-CVXHgUhv.mjs} +0 -0
@@ -13,13 +13,13 @@
13
13
  */
14
14
 
15
15
  const require_schemas = require('./schemas-nkIxa8sL.cjs');
16
- const require_webhooks = require('./webhooks-CbZpv9y_.cjs');
16
+ const require_webhooks = require('./webhooks-CLtDxwMa.cjs');
17
17
  const require_error = require('./error-Dn7ool6k.cjs');
18
18
  const require_logging = require('./logging-4s36JTiN.cjs');
19
19
  let camelcase = require("camelcase");
20
20
  camelcase = require_schemas.__toESM(camelcase);
21
- let _adobe_aio_commerce_lib_auth = require("@adobe/aio-commerce-lib-auth");
22
21
  let _adobe_aio_commerce_lib_api = require("@adobe/aio-commerce-lib-api");
22
+ let _adobe_aio_commerce_lib_auth = require("@adobe/aio-commerce-lib-auth");
23
23
  let _adobe_aio_commerce_lib_events_commerce = require("@adobe/aio-commerce-lib-events/commerce");
24
24
  let _adobe_aio_commerce_lib_events_io_events = require("@adobe/aio-commerce-lib-events/io-events");
25
25
  let _adobe_aio_commerce_lib_webhooks_api = require("@adobe/aio-commerce-lib-webhooks/api");
@@ -49,8 +49,8 @@ function isBranchStep(step) {
49
49
  * ```typescript
50
50
  * const createProviders = defineLeafStep({
51
51
  * name: "providers",
52
- * meta: { label: "Create Providers", description: "Creates I/O Events providers" },
53
- * run: async ({ config, stepContext }) => {
52
+ * meta: { install: { label: "Create Providers", description: "Creates I/O Events providers" } },
53
+ * install: async ({ config, stepContext }) => {
54
54
  * const { eventsClient } = stepContext;
55
55
  * return eventsClient.createProvider(config.eventing);
56
56
  * },
@@ -63,7 +63,8 @@ function defineLeafStep(options) {
63
63
  name: options.name,
64
64
  meta: options.meta,
65
65
  when: options.when,
66
- run: options.run,
66
+ install: options.install,
67
+ uninstall: options.uninstall,
67
68
  validate: options.validate
68
69
  };
69
70
  }
@@ -74,7 +75,7 @@ function defineLeafStep(options) {
74
75
  * ```typescript
75
76
  * const eventing = defineBranchStep({
76
77
  * name: "eventing",
77
- * meta: { label: "Eventing", description: "Sets up I/O Events" },
78
+ * meta: { install: { label: "Eventing", description: "Sets up I/O Events" } },
78
79
  * when: hasEventing,
79
80
  * context: async (ctx) => ({ eventsClient: await createEventsClient(ctx) }),
80
81
  * children: [commerceEventsStep, externalEventsStep],
@@ -155,12 +156,12 @@ function createFailedState(base, error) {
155
156
  * tree structure with all steps set to "pending".
156
157
  */
157
158
  function createInitialState(options) {
158
- const { rootStep, config } = options;
159
+ const { rootStep, config, mode } = options;
159
160
  return {
160
161
  id: crypto.randomUUID(),
161
162
  startedAt: nowIsoString(),
162
163
  status: "in-progress",
163
- step: buildInitialStepStatus(rootStep, config, []),
164
+ step: buildInitialStepStatus(rootStep, config, [], mode),
164
165
  data: null
165
166
  };
166
167
  }
@@ -168,6 +169,19 @@ function createInitialState(options) {
168
169
  * Executes a workflow from an initial state. Returns the final state (never throws).
169
170
  */
170
171
  async function executeWorkflow(options) {
172
+ return executeWorkflowWithMode(options, "install");
173
+ }
174
+ /**
175
+ * Executes an uninstall workflow from an initial state. Returns the final state (never throws).
176
+ * Steps with an `uninstall` handler get it called; steps without are silently skipped.
177
+ */
178
+ async function executeUninstallWorkflow(options) {
179
+ return executeWorkflowWithMode(options, "uninstall");
180
+ }
181
+ /**
182
+ * Internal implementation shared by executeWorkflow and executeUninstallWorkflow.
183
+ */
184
+ async function executeWorkflowWithMode(options, mode) {
171
185
  const { rootStep, installationContext, config, initialState, hooks } = options;
172
186
  const step = structuredClone(initialState.step);
173
187
  const context = {
@@ -178,7 +192,8 @@ async function executeWorkflow(options) {
178
192
  step,
179
193
  data: null,
180
194
  error: null,
181
- hooks
195
+ hooks,
196
+ mode
182
197
  };
183
198
  await callHook(hooks, "onInstallationStart", snapshot(context));
184
199
  try {
@@ -207,18 +222,18 @@ async function executeWorkflow(options) {
207
222
  * Builds initial step status from a step definition.
208
223
  * Filters steps based on their `when` conditions.
209
224
  */
210
- function buildInitialStepStatus(step, config, parentPath) {
225
+ function buildInitialStepStatus(step, config, parentPath, mode) {
211
226
  const path = [...parentPath, step.name];
212
227
  const children = [];
213
228
  if (isBranchStep(step) && step.children.length > 0) for (const child of step.children) {
214
229
  if (child.when && !child.when(config)) continue;
215
- children.push(buildInitialStepStatus(child, config, path));
230
+ children.push(buildInitialStepStatus(child, config, path, mode));
216
231
  }
217
232
  return {
218
233
  id: crypto.randomUUID(),
219
234
  name: step.name,
220
235
  path,
221
- meta: step.meta,
236
+ meta: mode === "uninstall" && step.meta.uninstall ? step.meta.uninstall : step.meta.install,
222
237
  status: "pending",
223
238
  children
224
239
  };
@@ -282,13 +297,17 @@ async function executeBranchStep(step, stepStatus, inherited, context) {
282
297
  await executeStep(childStep, child, childContext, context);
283
298
  }
284
299
  }
285
- /** Executes a leaf step and stores its result. */
300
+ /** Executes a leaf step and stores its result, or runs uninstall if in uninstall mode. */
286
301
  async function executeLeafStep(step, stepStatus, inherited, context) {
287
302
  const executionContext = {
288
303
  ...context.installationContext,
289
304
  ...inherited
290
305
  };
291
- const result = await step.run(context.config, executionContext);
306
+ if (context.mode === "uninstall") {
307
+ if (step.uninstall) await step.uninstall(context.config, executionContext);
308
+ return;
309
+ }
310
+ const result = await step.install(context.config, executionContext);
292
311
  context.data ??= {};
293
312
  setAtPath(context.data, stepStatus.path, result);
294
313
  }
@@ -349,7 +368,7 @@ async function validateStep(step, config, context, parentPath) {
349
368
  return {
350
369
  name: step.name,
351
370
  path,
352
- meta: step.meta,
371
+ meta: step.meta.install,
353
372
  issues,
354
373
  children
355
374
  };
@@ -413,6 +432,12 @@ function aggregateSummary(result) {
413
432
 
414
433
  //#endregion
415
434
  //#region source/management/installation/custom-installation/custom-scripts.ts
435
+ function isCustomInstallationStepDefinition(obj) {
436
+ return typeof obj === "object" && obj !== null && "install" in obj && typeof obj.install === "function";
437
+ }
438
+ function isCustomInstallationStepHandler(obj) {
439
+ return typeof obj === "function";
440
+ }
416
441
  /**
417
442
  * Creates a leaf step for executing a single custom installation script.
418
443
  */
@@ -420,26 +445,48 @@ function createCustomScriptStep(scriptConfig) {
420
445
  const { script, name, description } = scriptConfig;
421
446
  return defineLeafStep({
422
447
  name: (0, camelcase.default)(name),
423
- meta: {
448
+ meta: { install: {
424
449
  label: name,
425
450
  description
426
- },
427
- run: async (config, context) => {
451
+ } },
452
+ install: async (config, context) => {
428
453
  const { logger } = context;
429
454
  const customScripts = context.customScripts || {};
430
455
  logger.info(`Executing custom installation script: ${name}`);
431
456
  logger.debug(`Script path: ${script}`);
432
457
  const scriptModule = customScripts[script];
433
458
  if (!scriptModule) throw new Error(`Script ${script} not found in customScripts context. Make sure the script is defined in the configuration and the action was generated with custom scripts support.`);
434
- if (typeof scriptModule !== "object" || !("default" in scriptModule)) throw new Error(`Script ${script} must export a default function. Use defineCustomInstallationStep helper.`);
435
- const runFunction = scriptModule.default;
436
- if (typeof runFunction !== "function") throw new Error(`Script ${script} default export must be a function, got ${typeof runFunction}`);
459
+ if (typeof scriptModule !== "object" || !("default" in scriptModule)) throw new Error(`Script ${script} must export a default function or object. Use defineCustomInstallationStep helper.`);
460
+ const defaultExport = scriptModule.default;
461
+ let runFunction = null;
462
+ if (isCustomInstallationStepHandler(defaultExport)) runFunction = defaultExport;
463
+ else if (isCustomInstallationStepDefinition(defaultExport)) runFunction = defaultExport.install;
464
+ if (runFunction === null) throw new Error(`Script ${script} default export must be a function or an object with an install method. Use defineCustomInstallationStep helper.`);
437
465
  const scriptResult = await runFunction(config, context);
438
466
  logger.info(`Successfully executed script: ${name}`);
439
467
  return {
440
468
  script,
441
469
  data: scriptResult
442
470
  };
471
+ },
472
+ uninstall: async (config, context) => {
473
+ const { logger } = context;
474
+ const customScripts = context.customScripts || {};
475
+ logger.debug(`Uninstalling custom script: ${name}`);
476
+ const scriptModule = customScripts[script];
477
+ if (!scriptModule) throw new Error(`Script ${script} not found in customScripts context. Make sure the script is defined in the configuration and the action was generated with custom scripts support.`);
478
+ const defaultExport = scriptModule.default;
479
+ if (!isCustomInstallationStepDefinition(defaultExport)) {
480
+ logger.debug(`Script ${script} does not export an uninstall function, skipping uninstall.`);
481
+ return;
482
+ }
483
+ const { uninstall } = defaultExport;
484
+ if (!uninstall) {
485
+ logger.debug(`Script ${script} does not export an uninstall function, skipping uninstall.`);
486
+ return;
487
+ }
488
+ await uninstall(config, context);
489
+ logger.info(`Successfully uninstalled script: ${name}`);
443
490
  }
444
491
  });
445
492
  }
@@ -460,8 +507,14 @@ function createCustomScriptSteps(config) {
460
507
  const customInstallationStepBase = defineBranchStep({
461
508
  name: "customInstallationSteps",
462
509
  meta: {
463
- label: "Custom Installation Steps",
464
- description: "Executes custom installation scripts defined in the application configuration"
510
+ install: {
511
+ label: "Custom Installation Steps",
512
+ description: "Executes custom installation scripts defined in the application configuration"
513
+ },
514
+ uninstall: {
515
+ label: "Custom Uninstallation Steps",
516
+ description: "Executes custom uninstallation scripts defined in the application configuration"
517
+ }
465
518
  },
466
519
  when: require_webhooks.hasCustomInstallationSteps,
467
520
  children: []
@@ -483,34 +536,126 @@ function createCustomInstallationStep(config) {
483
536
  * Define a custom installation step with type-safe parameters.
484
537
  *
485
538
  * This helper provides type safety and IDE autocompletion for custom installation scripts.
486
- * The handler function receives properly typed `config` and `context` parameters.
539
+ * Accepts either a plain function (install only) or an object with `install` and optional
540
+ * `uninstall` handlers.
487
541
  *
488
- * @param handler - The installation step handler function
489
- * @returns The same handler function (for use as default export)
490
- *
491
- * @example
542
+ * @example Plain function (install only):
492
543
  * ```typescript
493
544
  * import { defineCustomInstallationStep } from "@adobe/aio-commerce-lib-app/management";
494
545
  *
495
546
  * export default defineCustomInstallationStep(async (config, context) => {
496
547
  * const { logger, params } = context;
497
- *
498
548
  * logger.info(`Setting up ${config.metadata.displayName}...`);
549
+ * return { status: "success" };
550
+ * });
551
+ * ```
499
552
  *
500
- * // Your installation logic here
501
- * // TypeScript will provide autocompletion for config and context
553
+ * @example Object form with install and uninstall:
554
+ * ```typescript
555
+ * import { defineCustomInstallationStep } from "@adobe/aio-commerce-lib-app/management";
502
556
  *
503
- * return {
504
- * status: "success",
505
- * message: "Setup completed",
506
- * };
557
+ * export default defineCustomInstallationStep({
558
+ * install: async (config, context) => {
559
+ * context.logger.info(`Registering ${config.metadata.displayName}...`);
560
+ * return { status: "success" };
561
+ * },
562
+ * uninstall: async (config, context) => {
563
+ * context.logger.info(`Removing ${config.metadata.displayName}...`);
564
+ * },
507
565
  * });
508
566
  * ```
509
567
  */
510
- function defineCustomInstallationStep(handler) {
511
- return handler;
568
+ function defineCustomInstallationStep(handlerOrDefinition) {
569
+ return handlerOrDefinition;
512
570
  }
513
571
 
572
+ //#endregion
573
+ //#region source/management/installation/admin-ui-sdk/helpers.ts
574
+ /**
575
+ * Registers the extension with Commerce via POST /V1/adminuisdk/extension.
576
+ *
577
+ * @param context - The execution context providing the Commerce HTTP client and logger.
578
+ * @returns The response from the Commerce API.
579
+ */
580
+ async function registerExtension(context) {
581
+ const { commerceClient, appData, logger } = context;
582
+ logger.info(`Registering Admin UI SDK extension: ${appData.projectName}`);
583
+ const response = await commerceClient.post("adminuisdk/extension", { json: { extension: {
584
+ extensionName: process.env.__OW_NAMESPACE,
585
+ extensionTitle: appData.projectTitle,
586
+ extensionUrl: `https://${process.env.__OW_NAMESPACE}.adobeio-static.net/index.html`,
587
+ extensionWorkspace: appData.workspaceName
588
+ } } }).json();
589
+ logger.info(`Admin UI SDK extension registered successfully: ${response.extensionId}`);
590
+ return response;
591
+ }
592
+ /**
593
+ * Unregisters the extension from Commerce via DELETE /V1/adminuisdk/extension/:workspace_name/:extension_name.
594
+ * Best-effort: errors are logged as warnings and do not stop the uninstall workflow.
595
+ *
596
+ * @param context - The execution context providing the Commerce HTTP client and logger.
597
+ */
598
+ async function uninstallExtension(context) {
599
+ const { commerceClient, appData, logger } = context;
600
+ const extensionName = process.env.__OW_NAMESPACE;
601
+ const endpoint = `adminuisdk/extension/${appData.workspaceName}/${extensionName}`;
602
+ logger.info(`Unregistering Admin UI SDK extension "${extensionName}" from workspace "${appData.workspaceName}"...`);
603
+ try {
604
+ await commerceClient.delete(endpoint);
605
+ logger.info(`Admin UI SDK extension "${extensionName}" unregistered successfully.`);
606
+ } catch (error) {
607
+ logger.warn(`Failed to unregister Admin UI SDK extension "${extensionName}": ${require_error.stringifyError(error)}. Continuing uninstall.`);
608
+ }
609
+ }
610
+
611
+ //#endregion
612
+ //#region source/management/installation/admin-ui-sdk/utils.ts
613
+ /** Creates the Admin UI SDK step context with a lazy-initialized Commerce HTTP client. */
614
+ const createAdminUiSdkStepContext = (installation) => {
615
+ const { params } = installation;
616
+ let commerceClient = null;
617
+ return { get commerceClient() {
618
+ if (commerceClient === null) commerceClient = new _adobe_aio_commerce_lib_api.AdobeCommerceHttpClient((0, _adobe_aio_commerce_lib_api.resolveCommerceHttpClientParams)(params, { tryForwardAuthProvider: true }));
619
+ return commerceClient;
620
+ } };
621
+ };
622
+
623
+ //#endregion
624
+ //#region source/management/installation/admin-ui-sdk/branch.ts
625
+ /** Leaf step that calls POST /V1/adminuisdk/extension to register the extension. */
626
+ const registerExtensionStep = defineLeafStep({
627
+ name: "register-extension",
628
+ meta: {
629
+ install: {
630
+ label: "Register Extension",
631
+ description: "Registers the Admin UI SDK extension in Adobe Commerce"
632
+ },
633
+ uninstall: {
634
+ label: "Unregister Extension",
635
+ description: "Removes the Admin UI SDK extension from Adobe Commerce"
636
+ }
637
+ },
638
+ install: (_, context) => registerExtension(context),
639
+ uninstall: (_, context) => uninstallExtension(context)
640
+ });
641
+ /** Branch step for setting up the Admin UI SDK extension registration. */
642
+ const adminUiSdkStep = defineBranchStep({
643
+ name: "admin-ui-sdk",
644
+ meta: {
645
+ install: {
646
+ label: "Admin UI SDK",
647
+ description: "Registers the extension with Adobe Commerce Admin UI SDK"
648
+ },
649
+ uninstall: {
650
+ label: "Admin UI SDK",
651
+ description: "Removes the extension from Adobe Commerce Admin UI SDK"
652
+ }
653
+ },
654
+ when: require_webhooks.hasAdminUiSdk,
655
+ context: createAdminUiSdkStepContext,
656
+ children: [registerExtensionStep]
657
+ });
658
+
514
659
  //#endregion
515
660
  //#region source/management/installation/events/utils.ts
516
661
  const COMMERCE_PROVIDER_TYPE = "dx_commerce_events";
@@ -519,12 +664,29 @@ const PROVIDER_TYPE_TO_LABEL = {
519
664
  [COMMERCE_PROVIDER_TYPE]: "Commerce",
520
665
  [EXTERNAL_PROVIDER_TYPE]: "External"
521
666
  };
667
+ /** Max characters taken from `metadata.id` in the I/O Events provider `instance_id`. */
668
+ const METADATA_ID_MAX_LENGTH_FOR_INSTANCE_ID = 100;
669
+ /**
670
+ * Generates a unique instance ID for I/O Events for this app deployment.
671
+ * Uses `{metadata.id (first 100 chars)}-{providerKeyOrSlug}-{workspaceId}` (lowercased).
672
+ *
673
+ * @param metadata - The metadata of the application
674
+ * @param provider - The event provider (optional `key`, else label is slugified)
675
+ * @param workspaceId - Adobe I/O Developer Console workspace ID for this deployment
676
+ */
677
+ function generateInstanceId(metadata, provider, workspaceId) {
678
+ const appId = metadata.id.slice(0, METADATA_ID_MAX_LENGTH_FOR_INSTANCE_ID);
679
+ const slugLabel = provider.label.toLowerCase().replace(/\s+/g, "-");
680
+ return `${appId}-${provider.key ?? slugLabel}-${workspaceId}`.toLowerCase();
681
+ }
522
682
  /**
523
- * Generates a unique instance ID for the given event provider within the context of the provided config.
683
+ * Old version of instanceId generator which can be not unique within the same ORG.
684
+ *
524
685
  * @param metadata - The metadata of the application
525
686
  * @param provider - The event provider for which to generate the instance ID
687
+ * @deprecated use {@link generateInstanceId} instead
526
688
  */
527
- function generateInstanceId(metadata, provider) {
689
+ function generateInstanceIdDeprecated(metadata, provider) {
528
690
  const slugLabel = provider.label.toLowerCase().replace(/\s+/g, "-");
529
691
  return `${metadata.id}-${provider.key ?? slugLabel}`.toLowerCase();
530
692
  }
@@ -545,7 +707,6 @@ function findExistingProviderMetadata(allMetadata, eventName) {
545
707
  return allMetadata.find((meta) => meta.event_code === eventName) ?? null;
546
708
  }
547
709
  /**
548
- <<<<<<< HEAD
549
710
  * Find existing event registrations by client ID and name.
550
711
  * @param allRegistrations - The list of all existing event registrations.
551
712
  * @param clientId - The client ID of the workspace where the registration was created.
@@ -576,7 +737,7 @@ function getIoEventCode(name, providerType) {
576
737
  * @param runtimeAction - The runtime action this registration points to.
577
738
  */
578
739
  function getRegistrationName(provider, runtimeAction) {
579
- const providerLabel = PROVIDER_TYPE_TO_LABEL[provider.provider_metadata] ?? "Unknown";
740
+ const providerLabel = PROVIDER_TYPE_TO_LABEL[provider.provider_metadata];
580
741
  const [packageName, actionName] = runtimeAction.split("/").map(kebabToTitleCase);
581
742
  return `${providerLabel} Event Registration: ${actionName} (${packageName})`;
582
743
  }
@@ -619,6 +780,37 @@ function findExistingSubscription(allSubscriptions, eventName) {
619
780
  return allSubscriptions.get(eventName) ?? null;
620
781
  }
621
782
  /**
783
+ * Builds the payload to send to Commerce when configuring Eventing.
784
+ * Returns `null` when no update call is needed.
785
+ *
786
+ * @param initialParams - Initial Commerce Eventing configuration parameters.
787
+ * @param existingData - Existing Commerce Eventing state from the API.
788
+ */
789
+ function getCommerceEventingConfigurationUpdateParams(initialParams, existingData) {
790
+ const { isDefaultProviderConfigured, isDefaultWorkspaceConfigurationEmpty } = existingData;
791
+ if (isDefaultProviderConfigured && !isDefaultWorkspaceConfigurationEmpty) return null;
792
+ const { workspace_configuration, ...configWithoutWorkspace } = initialParams;
793
+ let updateParams = { enabled: true };
794
+ if (isDefaultWorkspaceConfigurationEmpty) {
795
+ if (!workspace_configuration) throw new Error("Workspace configuration is required to enable Commerce Eventing when there is not an existing one.");
796
+ updateParams.workspace_configuration = workspace_configuration;
797
+ }
798
+ if (!isDefaultProviderConfigured) updateParams = {
799
+ ...updateParams,
800
+ ...configWithoutWorkspace
801
+ };
802
+ return updateParams;
803
+ }
804
+ /**
805
+ * Sanitizes a Commerce Eventing identifier.
806
+ * Preserves underscores, converts spaces to underscores, lowercases, and strips the rest.
807
+ *
808
+ * @param value - The raw identifier value to normalize.
809
+ */
810
+ function sanitizeEventingIdentifier(value) {
811
+ return value.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "");
812
+ }
813
+ /**
622
814
  * Creates a partially filled workspace configuration object based on the app credentials and parameters.
623
815
  * This configuration is used when creating an event provider in Commerce.
624
816
  *
@@ -700,8 +892,11 @@ async function getCommerceEventingExistingData(context) {
700
892
  const { commerceEventsClient } = context;
701
893
  const existingProviders = await commerceEventsClient.getAllEventProviders();
702
894
  const existingSubscriptions = await commerceEventsClient.getAllEventSubscriptions();
895
+ const defaultProvider = existingProviders.find((provider) => !("id" in provider)) ?? null;
896
+ const isDefaultProviderConfigured = defaultProvider !== null;
703
897
  return {
704
- isDefaultWorkspaceConfigurationEmpty: existingProviders.some((provider) => !("id" in provider) && !provider.workspace_configuration?.trim()),
898
+ isDefaultProviderConfigured,
899
+ isDefaultWorkspaceConfigurationEmpty: isDefaultProviderConfigured ? !defaultProvider.workspace_configuration?.trim() : true,
705
900
  providers: existingProviders,
706
901
  subscriptions: new Map(existingSubscriptions.map((subscription) => [subscription.name, subscription]))
707
902
  };
@@ -855,29 +1050,19 @@ async function createOrGetIoEventRegistration(params, registrations) {
855
1050
  }
856
1051
  /**
857
1052
  * Ensures Commerce Eventing is configured with the given configuration, updating it if it already exists.
858
- * @param eventsClient
859
- * @param params
860
- * @param existingData
1053
+ * @param params - The parameters necessary to configure Commerce Eventing.
1054
+ * @param existingData - Existing Commerce Eventing data.
861
1055
  */
862
1056
  async function configureCommerceEventing(params, existingData) {
863
1057
  const { context, config } = params;
864
1058
  const { commerceEventsClient, logger } = context;
865
1059
  logger.info("Starting configuration of the Commerce Eventing Module");
866
- let updateParams = {
867
- ...config,
868
- enabled: true
869
- };
870
- if (existingData.isDefaultWorkspaceConfigurationEmpty) {
871
- if (!config.workspace_configuration) {
872
- const message = "Workspace configuration is required to enable Commerce Eventing when there is not an existing one.";
873
- logger.error(message);
874
- throw new Error(message);
875
- }
876
- logger.info("Default provider workspace configuration already present, it will not be overriden");
877
- const { workspace_configuration, ...rest } = updateParams;
878
- updateParams = rest;
1060
+ const updateParams = getCommerceEventingConfigurationUpdateParams(config, existingData);
1061
+ if (updateParams === null) {
1062
+ logger.info("Commerce Eventing Module is already configured, skipping configuration step.");
1063
+ return;
879
1064
  }
880
- logger.info("Updating Commerce Eventing configuration with provided workspace configuration.");
1065
+ logger.info(`Updating Commerce Eventing Module configuration with the following data: [${Object.keys(updateParams).join(", ")}]`);
881
1066
  return commerceEventsClient.updateEventingConfiguration(updateParams).then((success) => {
882
1067
  if (success) {
883
1068
  logger.info("Commerce Eventing Module configured successfully.");
@@ -976,7 +1161,7 @@ async function createOrGetCommerceEventSubscription(params, existingData) {
976
1161
  async function onboardIoEvents(params, existingData) {
977
1162
  const { providersWithMetadata, registrations } = existingData;
978
1163
  const { context, metadata, provider, providerType, events } = params;
979
- const instanceId = generateInstanceId(metadata, provider);
1164
+ const instanceId = generateInstanceId(metadata, provider, context.appData.workspaceId);
980
1165
  const providerData = await createOrGetIoEventProvider({
981
1166
  context,
982
1167
  provider: {
@@ -1029,10 +1214,6 @@ async function onboardCommerceEventing(params, existingData) {
1029
1214
  const { events, provider, workspaceConfiguration } = ioData;
1030
1215
  const instanceId = provider.instance_id;
1031
1216
  const subscriptions = [];
1032
- await configureCommerceEventing({
1033
- context,
1034
- config: { workspace_configuration: workspaceConfiguration }
1035
- }, existingData);
1036
1217
  const { workspace_configuration: _, ...commerceProviderData } = await createOrGetCommerceProvider({
1037
1218
  context,
1038
1219
  provider: {
@@ -1054,6 +1235,181 @@ async function onboardCommerceEventing(params, existingData) {
1054
1235
  subscriptions
1055
1236
  };
1056
1237
  }
1238
+ /**
1239
+ * Deletes all I/O Events registrations for the given provider.
1240
+ * Registration names are reconstructed deterministically using the same logic as during installation.
1241
+ * Errors are caught and logged so that uninstall remains best-effort.
1242
+ */
1243
+ async function deleteIoEventRegistrations(providerData, provider, events, registrations, context) {
1244
+ const { ioEventsClient, appData, logger, params: runtimeParams } = context;
1245
+ const appCredentials = {
1246
+ consumerOrgId: appData.consumerOrgId,
1247
+ projectId: appData.projectId,
1248
+ workspaceId: appData.workspaceId
1249
+ };
1250
+ const actionEventsMap = groupEventsByRuntimeActions(events);
1251
+ const registrationNames = new Set(Array.from(actionEventsMap.keys()).map((runtimeAction) => getRegistrationName(providerData, runtimeAction)));
1252
+ const providerRegistrations = registrations.filter((reg) => reg.client_id === runtimeParams.AIO_COMMERCE_AUTH_IMS_CLIENT_ID && registrationNames.has(reg.name));
1253
+ if (providerRegistrations.length === 0) {
1254
+ logger.info(`No I/O Events registrations found for provider "${provider.label}" (instance ID: "${providerData.instance_id}").`);
1255
+ return;
1256
+ }
1257
+ logger.info(`Deleting ${providerRegistrations.length} I/O Events registration(s) for provider "${provider.label}" (instance ID: "${providerData.instance_id}")...`);
1258
+ for (const registration of providerRegistrations) {
1259
+ logger.info(`Deleting registration "${registration.name}" (ID: ${registration.id})...`);
1260
+ try {
1261
+ await ioEventsClient.deleteRegistration({
1262
+ ...appCredentials,
1263
+ registrationId: registration.registration_id
1264
+ });
1265
+ logger.info(`Deleted registration "${registration.name}" (ID: ${registration.id}).`);
1266
+ } catch (error) {
1267
+ logger.warn(`Failed to delete registration "${registration.name}" (ID: ${registration.id}): ${require_error.stringifyError(error)}. Continuing uninstall.`);
1268
+ }
1269
+ }
1270
+ }
1271
+ /**
1272
+ * Deletes all event metadata entries from the given I/O Events provider.
1273
+ * Errors are caught and logged so that uninstall remains best-effort.
1274
+ */
1275
+ async function deleteIoEventMetadata(providerData, provider, context) {
1276
+ const { ioEventsClient, appData, logger } = context;
1277
+ const appCredentials = {
1278
+ consumerOrgId: appData.consumerOrgId,
1279
+ projectId: appData.projectId,
1280
+ workspaceId: appData.workspaceId
1281
+ };
1282
+ const eventMetadataList = providerData.metadata ?? [];
1283
+ if (eventMetadataList.length === 0) {
1284
+ logger.info(`No event metadata found for provider "${provider.label}" (ID: ${providerData.id}).`);
1285
+ return;
1286
+ }
1287
+ logger.info(`Deleting ${eventMetadataList.length} event metadata entry(s) for provider "${provider.label}" (ID: ${providerData.id})...`);
1288
+ for (const eventMetadata of eventMetadataList) {
1289
+ logger.info(`Deleting event metadata "${eventMetadata.event_code}" from provider "${providerData.id}"...`);
1290
+ try {
1291
+ await ioEventsClient.deleteEventMetadataForProvider({
1292
+ ...appCredentials,
1293
+ providerId: providerData.id,
1294
+ eventCode: eventMetadata.event_code
1295
+ });
1296
+ logger.info(`Deleted event metadata "${eventMetadata.event_code}" from provider "${providerData.id}".`);
1297
+ } catch (error) {
1298
+ logger.warn(`Failed to delete event metadata "${eventMetadata.event_code}" from provider "${providerData.id}": ${require_error.stringifyError(error)}. Continuing uninstall.`);
1299
+ }
1300
+ }
1301
+ }
1302
+ /**
1303
+ * Deletes a single I/O Events provider.
1304
+ * Errors are caught and logged so that uninstall remains best-effort.
1305
+ */
1306
+ async function deleteIoEventProvider(providerData, provider, context) {
1307
+ const { ioEventsClient, appData, logger } = context;
1308
+ const appCredentials = {
1309
+ consumerOrgId: appData.consumerOrgId,
1310
+ projectId: appData.projectId,
1311
+ workspaceId: appData.workspaceId
1312
+ };
1313
+ logger.info(`Deleting I/O Events provider "${provider.label}" (ID: ${providerData.id})...`);
1314
+ try {
1315
+ await ioEventsClient.deleteEventProvider({
1316
+ ...appCredentials,
1317
+ providerId: providerData.id
1318
+ });
1319
+ logger.info(`Deleted I/O Events provider "${provider.label}" (ID: ${providerData.id}).`);
1320
+ } catch (error) {
1321
+ logger.warn(`Failed to delete I/O Events provider "${provider.label}" (ID: ${providerData.id}): ${require_error.stringifyError(error)}. Continuing uninstall.`);
1322
+ }
1323
+ }
1324
+ /**
1325
+ * Offboards a single event source from I/O Events by deleting, in order:
1326
+ * 1. All registrations that reference events from this provider.
1327
+ * 2. All event metadata entries on the provider.
1328
+ * 3. The provider itself.
1329
+ *
1330
+ * This is the reverse of {@link onboardIoEvents} and is called during uninstall.
1331
+ * All deletion errors are caught and logged so that uninstall remains best-effort.
1332
+ *
1333
+ * @param params - Configuration identifying the provider to offboard.
1334
+ * @param existingData - Current I/O Events data (providers and registrations).
1335
+ */
1336
+ async function offboardIoEvents(params, existingData) {
1337
+ const { context, metadata, provider, events } = params;
1338
+ const { appData, logger } = context;
1339
+ const instanceId = generateInstanceId(metadata, provider, appData.workspaceId);
1340
+ const instanceIdOldVersion = generateInstanceIdDeprecated(metadata, provider);
1341
+ const providerData = existingData.providersWithMetadata.find((p) => p.instance_id === instanceId || p.instance_id === instanceIdOldVersion);
1342
+ if (!providerData) {
1343
+ logger.info(`No I/O Events provider found with instance ID "${instanceId}", skipping offboarding.`);
1344
+ return;
1345
+ }
1346
+ await deleteIoEventRegistrations(providerData, provider, events, existingData.registrations, context);
1347
+ await deleteIoEventMetadata(providerData, provider, context);
1348
+ await deleteIoEventProvider(providerData, provider, context);
1349
+ }
1350
+ /**
1351
+ * Deletes all Commerce event subscriptions for the given events.
1352
+ * Subscriptions are matched by their namespaced name, built the same way as during installation.
1353
+ * Errors are caught and logged so that uninstall remains best-effort.
1354
+ */
1355
+ async function deleteCommerceEventSubscriptions(events, metadata, provider, existingSubscriptions, context) {
1356
+ const { commerceEventsClient, logger } = context;
1357
+ logger.info(`Unsubscribing Commerce event subscriptions for provider "${provider.label}"...`);
1358
+ for (const event of events) {
1359
+ const eventName = getNamespacedEvent(metadata, event.name);
1360
+ if (!existingSubscriptions.has(eventName)) {
1361
+ logger.info(`No Commerce subscription found for event "${event.name}" (namespaced: "${eventName}"), skipping.`);
1362
+ continue;
1363
+ }
1364
+ logger.info(`Unsubscribing Commerce event subscription for "${event.name}" (namespaced: "${eventName}")...`);
1365
+ try {
1366
+ await commerceEventsClient.deleteEventSubscription({ name: eventName });
1367
+ logger.info(`Unsubscribed Commerce event subscription for "${eventName}".`);
1368
+ } catch (error) {
1369
+ logger.warn(`Failed to unsubscribe Commerce event subscription for "${eventName}": ${require_error.stringifyError(error)}. Continuing uninstall.`);
1370
+ }
1371
+ }
1372
+ }
1373
+ /**
1374
+ * Deletes a single Commerce-side event provider.
1375
+ * The provider is matched by its deterministic `instance_id`. If not found, deletion is skipped.
1376
+ * Errors are caught and logged so that uninstall remains best-effort.
1377
+ */
1378
+ async function deleteCommerceEventProvider(metadata, provider, existingProviders, context) {
1379
+ const { commerceEventsClient, appData, logger } = context;
1380
+ const instanceId = generateInstanceId(metadata, provider, appData.workspaceId);
1381
+ const instanceIdOldVersion = generateInstanceIdDeprecated(metadata, provider);
1382
+ const commerceProvider = existingProviders.find((p) => p.instance_id === instanceId || p.instance_id === instanceIdOldVersion);
1383
+ if (!commerceProvider) {
1384
+ logger.info(`No Commerce event provider found with instance ID "${instanceId}", skipping provider deletion.`);
1385
+ return;
1386
+ }
1387
+ logger.info(`Deleting Commerce event provider "${provider.label}" (provider_id: ${commerceProvider.provider_id})...`);
1388
+ try {
1389
+ await commerceEventsClient.deleteEventProvider({ provider_id: commerceProvider.provider_id });
1390
+ logger.info(`Deleted Commerce event provider "${provider.label}" (provider_id: ${commerceProvider.provider_id}).`);
1391
+ } catch (error) {
1392
+ logger.warn(`Failed to delete Commerce event provider "${provider.label}" (provider_id: ${commerceProvider.provider_id}): ${require_error.stringifyError(error)}. Continuing uninstall.`);
1393
+ }
1394
+ }
1395
+ /**
1396
+ * Offboards Commerce eventing for a single provider. Performs the following steps in order:
1397
+ * 1. Unsubscribes all event subscriptions that were created for the given provider.
1398
+ * 2. Deletes the Commerce-side event provider itself.
1399
+ *
1400
+ * Subscriptions are matched by their namespaced name, which is deterministic and built the
1401
+ * same way as during {@link onboardCommerceEventing}. The provider is matched by its
1402
+ * `instance_id`. Missing subscriptions or providers are silently skipped. All errors are
1403
+ * caught and logged so that uninstall remains best-effort.
1404
+ *
1405
+ * @param params - Configuration identifying the provider and its events to offboard.
1406
+ * @param existingData - Current Commerce eventing data (providers and subscriptions).
1407
+ */
1408
+ async function offboardCommerceEventing(params, existingData) {
1409
+ const { context, metadata, provider, events } = params;
1410
+ await deleteCommerceEventSubscriptions(events, metadata, provider, existingData.subscriptions, context);
1411
+ await deleteCommerceEventProvider(metadata, provider, existingData.providers, context);
1412
+ }
1057
1413
 
1058
1414
  //#endregion
1059
1415
  //#region source/management/installation/events/commerce.ts
@@ -1061,56 +1417,105 @@ async function onboardCommerceEventing(params, existingData) {
1061
1417
  const commerceEventsStep = defineLeafStep({
1062
1418
  name: "commerce",
1063
1419
  meta: {
1064
- label: "Configure Commerce Events",
1065
- description: "Sets up I/O Events for Adobe Commerce event sources"
1420
+ install: {
1421
+ label: "Configure Commerce Events",
1422
+ description: "Sets up I/O Events for Adobe Commerce event sources"
1423
+ },
1424
+ uninstall: {
1425
+ label: "Remove Commerce Events",
1426
+ description: "Removes I/O Events for Adobe Commerce event sources"
1427
+ }
1066
1428
  },
1067
1429
  when: require_webhooks.hasCommerceEvents,
1068
- run: async (config, context) => {
1069
- const { logger } = context;
1070
- logger.debug("Starting installation of Commerce Events with config:", config);
1071
- const stepData = [];
1072
- const workspaceConfiguration = JSON.stringify(makeWorkspaceConfig(context));
1073
- const existingIoEventsData = await getIoEventsExistingData(context);
1074
- const commerceEventingExistingData = await getCommerceEventingExistingData(context);
1075
- for (const { provider, events } of config.eventing.commerce) {
1076
- const { providerData, eventsData } = await onboardIoEvents({
1077
- context,
1078
- metadata: config.metadata,
1079
- provider,
1080
- events,
1081
- providerType: COMMERCE_PROVIDER_TYPE
1082
- }, existingIoEventsData);
1083
- const { commerceProvider, subscriptions } = await onboardCommerceEventing({
1084
- context,
1085
- metadata: config.metadata,
1086
- provider,
1087
- ioData: {
1088
- provider: providerData,
1089
- events: eventsData,
1090
- workspaceConfiguration
1091
- }
1092
- }, commerceEventingExistingData);
1093
- stepData.push({ provider: {
1094
- config: provider,
1095
- data: {
1096
- ioEvents: providerData,
1097
- commerce: commerceProvider,
1098
- events: eventsData.map(({ config, data }, index) => {
1099
- return {
1100
- config,
1101
- data: {
1102
- ...data,
1103
- subscription: subscriptions[index]
1104
- }
1105
- };
1106
- })
1107
- }
1108
- } });
1109
- }
1110
- logger.debug("Completed Commerce Events installation step.");
1111
- return stepData;
1112
- }
1430
+ install: createCommerceEvents,
1431
+ uninstall: removeCommerceEvents
1113
1432
  });
1433
+ /**
1434
+ * Creates all needed entities for Eventing to work with Commerce and Adobe I/O Events.
1435
+ * @param config - The configuration of the app, with commerce events.
1436
+ * @param context - The execution context for the events installation.
1437
+ */
1438
+ async function createCommerceEvents(config, context) {
1439
+ const { logger } = context;
1440
+ logger.debug("Starting installation of Commerce Events with config:", config);
1441
+ const stepData = [];
1442
+ const workspaceConfiguration = JSON.stringify(makeWorkspaceConfig(context));
1443
+ const existingIoEventsData = await getIoEventsExistingData(context);
1444
+ const commerceEventingExistingData = await getCommerceEventingExistingData(context);
1445
+ for (let i = 0; i < config.eventing.commerce.length; i++) {
1446
+ const { provider, events } = config.eventing.commerce[i];
1447
+ const { providerData, eventsData } = await onboardIoEvents({
1448
+ context,
1449
+ metadata: config.metadata,
1450
+ provider,
1451
+ events,
1452
+ providerType: COMMERCE_PROVIDER_TYPE
1453
+ }, existingIoEventsData);
1454
+ if (i === 0) await configureCommerceEventing({
1455
+ context,
1456
+ config: {
1457
+ enabled: true,
1458
+ merchant_id: sanitizeEventingIdentifier(context.appData.orgName),
1459
+ environment_id: sanitizeEventingIdentifier(context.appData.projectName),
1460
+ instance_id: providerData.instance_id,
1461
+ workspace_configuration: workspaceConfiguration
1462
+ }
1463
+ }, commerceEventingExistingData);
1464
+ const { commerceProvider, subscriptions } = await onboardCommerceEventing({
1465
+ context,
1466
+ metadata: config.metadata,
1467
+ provider,
1468
+ ioData: {
1469
+ provider: providerData,
1470
+ events: eventsData,
1471
+ workspaceConfiguration
1472
+ }
1473
+ }, commerceEventingExistingData);
1474
+ stepData.push({ provider: {
1475
+ config: provider,
1476
+ data: {
1477
+ ioEvents: providerData,
1478
+ commerce: commerceProvider,
1479
+ events: eventsData.map(({ config, data }, index) => {
1480
+ return {
1481
+ config,
1482
+ data: {
1483
+ ...data,
1484
+ subscription: subscriptions[index]
1485
+ }
1486
+ };
1487
+ })
1488
+ }
1489
+ } });
1490
+ }
1491
+ logger.debug("Completed Commerce Events installation step.");
1492
+ return stepData;
1493
+ }
1494
+ /**
1495
+ * Remove all created for Commerce eventing created durint installation
1496
+ * @param config - The configuration of the app, with commerce events.
1497
+ * @param context - The execution context for the events installation.
1498
+ */
1499
+ async function removeCommerceEvents(config, context) {
1500
+ const { logger } = context;
1501
+ logger.debug("Starting uninstall of Commerce Events with config:", config);
1502
+ const [existingIoEventsData, commerceEventingExistingData] = await Promise.all([getIoEventsExistingData(context), getCommerceEventingExistingData(context)]);
1503
+ for (const { provider, events } of config.eventing.commerce) {
1504
+ await offboardCommerceEventing({
1505
+ context,
1506
+ metadata: config.metadata,
1507
+ provider,
1508
+ events
1509
+ }, commerceEventingExistingData);
1510
+ await offboardIoEvents({
1511
+ context,
1512
+ metadata: config.metadata,
1513
+ provider,
1514
+ events
1515
+ }, existingIoEventsData);
1516
+ }
1517
+ logger.debug("Completed Commerce Events uninstall step.");
1518
+ }
1114
1519
 
1115
1520
  //#endregion
1116
1521
  //#region source/management/installation/events/context.ts
@@ -1126,6 +1531,8 @@ function createCommerceEventsApiClient(params) {
1126
1531
  createEventProvider: _adobe_aio_commerce_lib_events_commerce.createEventProvider,
1127
1532
  getAllEventProviders: _adobe_aio_commerce_lib_events_commerce.getAllEventProviders,
1128
1533
  createEventSubscription: _adobe_aio_commerce_lib_events_commerce.createEventSubscription,
1534
+ deleteEventProvider: _adobe_aio_commerce_lib_events_commerce.deleteEventProvider,
1535
+ deleteEventSubscription: _adobe_aio_commerce_lib_events_commerce.deleteEventSubscription,
1129
1536
  getAllEventSubscriptions: _adobe_aio_commerce_lib_events_commerce.getAllEventSubscriptions,
1130
1537
  updateEventingConfiguration: _adobe_aio_commerce_lib_events_commerce.updateEventingConfiguration
1131
1538
  });
@@ -1142,6 +1549,9 @@ function createIoEventsApiClient(params) {
1142
1549
  createEventProvider: _adobe_aio_commerce_lib_events_io_events.createEventProvider,
1143
1550
  createEventMetadataForProvider: _adobe_aio_commerce_lib_events_io_events.createEventMetadataForProvider,
1144
1551
  createRegistration: _adobe_aio_commerce_lib_events_io_events.createRegistration,
1552
+ deleteEventMetadataForProvider: _adobe_aio_commerce_lib_events_io_events.deleteEventMetadataForProvider,
1553
+ deleteEventProvider: _adobe_aio_commerce_lib_events_io_events.deleteEventProvider,
1554
+ deleteRegistration: _adobe_aio_commerce_lib_events_io_events.deleteRegistration,
1145
1555
  getAllEventProviders: _adobe_aio_commerce_lib_events_io_events.getAllEventProviders,
1146
1556
  getAllRegistrations: _adobe_aio_commerce_lib_events_io_events.getAllRegistrations
1147
1557
  });
@@ -1169,38 +1579,68 @@ function createEventsStepContext(installation) {
1169
1579
  const externalEventsStep = defineLeafStep({
1170
1580
  name: "external",
1171
1581
  meta: {
1172
- label: "Configure External Events",
1173
- description: "Sets up I/O Events for external event sources"
1582
+ install: {
1583
+ label: "Configure External Events",
1584
+ description: "Sets up I/O Events for external event sources"
1585
+ },
1586
+ uninstall: {
1587
+ label: "Remove External Events",
1588
+ description: "Removes I/O Events for external event sources"
1589
+ }
1174
1590
  },
1175
1591
  when: require_webhooks.hasExternalEvents,
1176
- run: async (config, context) => {
1177
- const { logger } = context;
1178
- logger.debug("Starting installation of External Events with config:", config);
1179
- const stepData = [];
1180
- const existingIoEventsData = await getIoEventsExistingData(context);
1181
- for (const { provider, events } of config.eventing.external) {
1182
- const { providerData, eventsData } = await onboardIoEvents({
1183
- context,
1184
- metadata: config.metadata,
1185
- provider,
1186
- events,
1187
- providerType: EXTERNAL_PROVIDER_TYPE
1188
- }, existingIoEventsData);
1189
- stepData.push({ provider: {
1190
- config: provider,
1191
- data: {
1192
- ioEvents: providerData,
1193
- events: {
1194
- config: events,
1195
- data: eventsData
1196
- }
1592
+ install: createExternalEvents,
1593
+ uninstall: removeExternalEvents
1594
+ });
1595
+ /**
1596
+ * Creates all needed entities for External Events to work with Adobe I/O Events.
1597
+ * @param config - The configuration of the app, with external events.
1598
+ * @param context - The execution context for the events installation.
1599
+ */
1600
+ async function createExternalEvents(config, context) {
1601
+ const { logger } = context;
1602
+ logger.debug("Starting installation of External Events with config:", config);
1603
+ const stepData = [];
1604
+ const existingIoEventsData = await getIoEventsExistingData(context);
1605
+ for (const { provider, events } of config.eventing.external) {
1606
+ const { providerData, eventsData } = await onboardIoEvents({
1607
+ context,
1608
+ metadata: config.metadata,
1609
+ provider,
1610
+ events,
1611
+ providerType: EXTERNAL_PROVIDER_TYPE
1612
+ }, existingIoEventsData);
1613
+ stepData.push({ provider: {
1614
+ config: provider,
1615
+ data: {
1616
+ ioEvents: providerData,
1617
+ events: {
1618
+ config: events,
1619
+ data: eventsData
1197
1620
  }
1198
- } });
1199
- }
1200
- logger.debug("Completed External Events installation step.");
1201
- return stepData;
1621
+ }
1622
+ } });
1202
1623
  }
1203
- });
1624
+ logger.debug("Completed External Events installation step.");
1625
+ return stepData;
1626
+ }
1627
+ /**
1628
+ * Removed all created entities for External Events during the installation
1629
+ * @param config - The configuration of the app, with external events.
1630
+ * @param context - The execution context for the events installation.
1631
+ */
1632
+ async function removeExternalEvents(config, context) {
1633
+ const { logger } = context;
1634
+ logger.debug("Starting uninstall of External Events with config:", config);
1635
+ const existingIoEventsData = await getIoEventsExistingData(context);
1636
+ for (const { provider, events } of config.eventing.external) await offboardIoEvents({
1637
+ context,
1638
+ metadata: config.metadata,
1639
+ provider,
1640
+ events
1641
+ }, existingIoEventsData);
1642
+ logger.debug("Completed External Events uninstall step.");
1643
+ }
1204
1644
 
1205
1645
  //#endregion
1206
1646
  //#region source/management/installation/events/branch.ts
@@ -1208,8 +1648,14 @@ const externalEventsStep = defineLeafStep({
1208
1648
  const eventingStep = defineBranchStep({
1209
1649
  name: "eventing",
1210
1650
  meta: {
1211
- label: "Eventing",
1212
- description: "Sets up the I/O Events and the Commerce events required by the application"
1651
+ install: {
1652
+ label: "Eventing",
1653
+ description: "Sets up the I/O Events and the Commerce events required by the application"
1654
+ },
1655
+ uninstall: {
1656
+ label: "Eventing",
1657
+ description: "Removes the I/O Events and Commerce events configured by the application"
1658
+ }
1213
1659
  },
1214
1660
  when: require_webhooks.hasEventing,
1215
1661
  context: createEventsStepContext,
@@ -1228,7 +1674,8 @@ function createCommerceWebhooksApiClient(params) {
1228
1674
  commerceClientParams.fetchOptions.timeout = 1e3 * 60 * 2;
1229
1675
  return (0, _adobe_aio_commerce_lib_webhooks_api.createCustomCommerceWebhooksApiClient)(commerceClientParams, {
1230
1676
  getWebhookList: _adobe_aio_commerce_lib_webhooks_api.getWebhookList,
1231
- subscribeWebhook: _adobe_aio_commerce_lib_webhooks_api.subscribeWebhook
1677
+ subscribeWebhook: _adobe_aio_commerce_lib_webhooks_api.subscribeWebhook,
1678
+ unsubscribeWebhook: _adobe_aio_commerce_lib_webhooks_api.unsubscribeWebhook
1232
1679
  });
1233
1680
  }
1234
1681
  /** Creates the webhooks step context with a lazy-initialized API client. */
@@ -1326,11 +1773,49 @@ async function createWebhookSubscriptions(config, context) {
1326
1773
  return { subscribedWebhooks };
1327
1774
  }
1328
1775
  /**
1776
+ * Unsubscribes each webhook from the app config in Adobe Commerce.
1777
+ * If a webhook is not found in the existing list, it is silently skipped (idempotent).
1778
+ *
1779
+ * @param config - The app config (must have a non-empty `webhooks` array).
1780
+ * @param context - The webhooks execution context (provides the Commerce API client and logger).
1781
+ */
1782
+ async function deleteWebhookSubscriptions(config, context) {
1783
+ const { logger, commerceWebhooksClient } = context;
1784
+ logger.info(`Unsubscribing ${config.webhooks.length} webhook(s) from Commerce...`);
1785
+ const idPrefix = buildWebhookIdPrefix(config.metadata.id);
1786
+ const unsubscribedWebhooks = [];
1787
+ const existingWebhooks = await commerceWebhooksClient.getWebhookList();
1788
+ for (const entry of config.webhooks) {
1789
+ const { webhook } = entry;
1790
+ const resolvedBatch = `${idPrefix}${webhook.batch_name}`;
1791
+ const resolvedHook = `${idPrefix}${webhook.hook_name}`;
1792
+ const params = {
1793
+ webhook_method: webhook.webhook_method,
1794
+ webhook_type: webhook.webhook_type,
1795
+ batch_name: resolvedBatch,
1796
+ hook_name: resolvedHook
1797
+ };
1798
+ if (!isWebhookInList(existingWebhooks, params)) {
1799
+ logger.debug(`Webhook not found, skipping unsubscribe: ${getWebhookName(webhook)}`);
1800
+ continue;
1801
+ }
1802
+ try {
1803
+ await deleteWebhookSubscription(commerceWebhooksClient, webhook, params);
1804
+ logger.info(`Unsubscribed webhook: ${getWebhookName(webhook)}`);
1805
+ unsubscribedWebhooks.push(params);
1806
+ } catch (error) {
1807
+ logger.warn(`Failed to unsubscribe webhook "${getWebhookName(webhook)}": ${require_error.stringifyError(error)}. Continuing uninstall.`);
1808
+ }
1809
+ }
1810
+ logger.info(`Webhook unsubscriptions complete: ${unsubscribedWebhooks.length} unsubscribed.`);
1811
+ return { unsubscribedWebhooks };
1812
+ }
1813
+ /**
1329
1814
  * Subscribes a single webhook to Commerce, skipping the API call if the webhook
1330
1815
  * is already subscribed (matched by webhook_method, webhook_type, batch_name, hook_name).
1331
1816
  */
1332
1817
  async function createOrGetWebhookSubscription(existingWebhooks, client, resolvedWebhook, logger) {
1333
- if (isAlreadySubscribed(existingWebhooks, resolvedWebhook)) {
1818
+ if (isWebhookInList(existingWebhooks, resolvedWebhook)) {
1334
1819
  logger.info(`Webhook already subscribed, skipping: ${getWebhookName(resolvedWebhook)}`);
1335
1820
  return resolvedWebhook;
1336
1821
  }
@@ -1339,6 +1824,22 @@ async function createOrGetWebhookSubscription(existingWebhooks, client, resolved
1339
1824
  return subscribed;
1340
1825
  }
1341
1826
  /**
1827
+ * Re-throws `err`, enriching the message with the webhook name if the error is an
1828
+ * `HTTPError` with a JSON body containing a string `message` field.
1829
+ */
1830
+ async function rethrowWithWebhookName(err, webhookName, operation) {
1831
+ if (err instanceof ky.HTTPError) {
1832
+ let body;
1833
+ try {
1834
+ body = await err.response.json();
1835
+ } catch {
1836
+ throw err;
1837
+ }
1838
+ if (typeof body?.message === "string") throw new Error(`Webhook ${operation} failed for "${webhookName}": ${body.message}`);
1839
+ }
1840
+ throw err;
1841
+ }
1842
+ /**
1342
1843
  * Subscribes a single webhook to Commerce, enriching the error with the webhook name
1343
1844
  * if the API responds with a string `message`.
1344
1845
  */
@@ -1347,16 +1848,18 @@ async function createWebhookSubscription(client, resolvedWebhook) {
1347
1848
  await client.subscribeWebhook(resolvedWebhook);
1348
1849
  return resolvedWebhook;
1349
1850
  } catch (err) {
1350
- if (err instanceof ky.HTTPError) {
1351
- let body;
1352
- try {
1353
- body = await err.response.json();
1354
- } catch {
1355
- throw err;
1356
- }
1357
- if (typeof body?.message === "string") throw new Error(`Webhook subscription failed for "${getWebhookName(resolvedWebhook)}": ${body.message}`);
1358
- }
1359
- throw err;
1851
+ return await rethrowWithWebhookName(err, getWebhookName(resolvedWebhook), "subscription");
1852
+ }
1853
+ }
1854
+ /**
1855
+ * Unsubscribes a single webhook from Commerce, enriching the error with the webhook name
1856
+ * if the API responds with a string `message`.
1857
+ */
1858
+ async function deleteWebhookSubscription(client, resolvedWebhook, params) {
1859
+ try {
1860
+ await client.unsubscribeWebhook(params);
1861
+ } catch (err) {
1862
+ await rethrowWithWebhookName(err, getWebhookName(resolvedWebhook), "unsubscription");
1360
1863
  }
1361
1864
  }
1362
1865
  /**
@@ -1377,14 +1880,14 @@ function resolveDeveloperConsoleOAuthCredentials(params) {
1377
1880
  };
1378
1881
  }
1379
1882
  /**
1380
- * Returns true when the candidate webhook is already present in the existing subscription list,
1381
- * matched by the four-part identity: webhook_method, webhook_type, batch_name, hook_name.
1883
+ * Returns true when a webhook with the given four-part identity exists in the list.
1382
1884
  *
1885
+ * The identity check uses: webhook_method, webhook_type, batch_name, hook_name.
1383
1886
  * `webhook_method` is normalised before comparison to handle the case where Commerce strips the
1384
1887
  * `.magento` segment from plugin webhook methods on storage
1385
1888
  * (e.g. `plugin.magento.foo` and `plugin.foo` are treated as the same method).
1386
1889
  */
1387
- function isAlreadySubscribed(existing, candidate) {
1890
+ function isWebhookInList(existing, candidate) {
1388
1891
  const normalizedCandidate = normalizeWebhookMethod(candidate.webhook_method);
1389
1892
  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);
1390
1893
  }
@@ -1442,18 +1945,33 @@ function getWebhookName(webhook) {
1442
1945
  const subscriptionsStep = defineLeafStep({
1443
1946
  name: "subscriptions",
1444
1947
  meta: {
1445
- label: "Create Subscriptions",
1446
- description: "Creates webhook subscriptions in Adobe Commerce"
1948
+ install: {
1949
+ label: "Create Subscriptions",
1950
+ description: "Creates webhook subscriptions in Adobe Commerce"
1951
+ },
1952
+ uninstall: {
1953
+ label: "Delete Subscriptions",
1954
+ description: "Deletes webhook subscriptions from Adobe Commerce"
1955
+ }
1447
1956
  },
1448
1957
  validate: (config, context) => validateWebhookConflicts(config, context),
1449
- run: (config, context) => createWebhookSubscriptions(config, context)
1958
+ install: (config, context) => createWebhookSubscriptions(config, context),
1959
+ uninstall: async (config, context) => {
1960
+ await deleteWebhookSubscriptions(config, context);
1961
+ }
1450
1962
  });
1451
1963
  /** Branch step for setting up Commerce webhooks. */
1452
1964
  const webhooksStep = defineBranchStep({
1453
1965
  name: "webhooks",
1454
1966
  meta: {
1455
- label: "Webhooks",
1456
- description: "Sets up Commerce webhooks"
1967
+ install: {
1968
+ label: "Webhooks",
1969
+ description: "Sets up Commerce webhooks"
1970
+ },
1971
+ uninstall: {
1972
+ label: "Webhooks",
1973
+ description: "Removes Commerce webhooks"
1974
+ }
1457
1975
  },
1458
1976
  when: require_webhooks.hasWebhooks,
1459
1977
  context: createWebhooksStepContext,
@@ -1469,6 +1987,7 @@ function createDefaultChildSteps(config) {
1469
1987
  return [
1470
1988
  eventingStep,
1471
1989
  webhooksStep,
1990
+ adminUiSdkStep,
1472
1991
  createCustomInstallationStep(config)
1473
1992
  ];
1474
1993
  }
@@ -1478,10 +1997,23 @@ function createDefaultChildSteps(config) {
1478
1997
  function createRootInstallationStep(config) {
1479
1998
  return defineBranchStep({
1480
1999
  name: "installation",
1481
- meta: {
2000
+ meta: { install: {
1482
2001
  label: "Installation",
1483
2002
  description: "App installation workflow"
1484
- },
2003
+ } },
2004
+ children: createDefaultChildSteps(config)
2005
+ });
2006
+ }
2007
+ /**
2008
+ * Creates a root uninstallation step with dynamic children based on the config.
2009
+ */
2010
+ function createRootUninstallationStep(config) {
2011
+ return defineBranchStep({
2012
+ name: "uninstallation",
2013
+ meta: { install: {
2014
+ label: "Uninstallation",
2015
+ description: "App uninstallation workflow"
2016
+ } },
1485
2017
  children: createDefaultChildSteps(config)
1486
2018
  });
1487
2019
  }
@@ -1514,6 +2046,30 @@ function runInstallation(options) {
1514
2046
  });
1515
2047
  }
1516
2048
  /**
2049
+ * Creates an initial uninstallation state from the config and step definitions.
2050
+ */
2051
+ function createInitialUninstallationState(options) {
2052
+ const { config } = options;
2053
+ return createInitialState({
2054
+ rootStep: createRootUninstallationStep(config),
2055
+ config,
2056
+ mode: "uninstall"
2057
+ });
2058
+ }
2059
+ /**
2060
+ * Runs the full uninstallation workflow. Returns the final state (never throws).
2061
+ */
2062
+ function runUninstallation(options) {
2063
+ const { installationContext, config, initialState, hooks } = options;
2064
+ return executeUninstallWorkflow({
2065
+ rootStep: createRootUninstallationStep(config),
2066
+ installationContext,
2067
+ config,
2068
+ initialState,
2069
+ hooks
2070
+ });
2071
+ }
2072
+ /**
1517
2073
  * Runs pre-installation validation over the full step tree.
1518
2074
  *
1519
2075
  * Traverses the same step hierarchy used during installation but only calls
@@ -1537,6 +2093,12 @@ Object.defineProperty(exports, 'createInitialInstallationState', {
1537
2093
  return createInitialInstallationState;
1538
2094
  }
1539
2095
  });
2096
+ Object.defineProperty(exports, 'createInitialUninstallationState', {
2097
+ enumerable: true,
2098
+ get: function () {
2099
+ return createInitialUninstallationState;
2100
+ }
2101
+ });
1540
2102
  Object.defineProperty(exports, 'defineCustomInstallationStep', {
1541
2103
  enumerable: true,
1542
2104
  get: function () {
@@ -1573,6 +2135,12 @@ Object.defineProperty(exports, 'runInstallation', {
1573
2135
  return runInstallation;
1574
2136
  }
1575
2137
  });
2138
+ Object.defineProperty(exports, 'runUninstallation', {
2139
+ enumerable: true,
2140
+ get: function () {
2141
+ return runUninstallation;
2142
+ }
2143
+ });
1576
2144
  Object.defineProperty(exports, 'runValidation', {
1577
2145
  enumerable: true,
1578
2146
  get: function () {