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