@go-to-k/cdkd 0.143.0 → 0.145.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.
package/dist/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { _ as withSkipPrefix, a as runDockerStreaming, c as getLogger, d as getLiveRenderer, f as PATTERN_B_NAME_PROPERTIES, g as generateResourceNameWithFallback, h as generateResourceName, i as runDockerForeground, n as formatDockerLoginError, p as PATTERN_B_RESOURCE_TYPES, r as getDockerCmd, u as runStackBuffered, v as withStackName } from "./docker-cmd-EtWSTAje.js";
3
- import { A as AssetPublisher, B as resolveStateBucketWithDefault, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as getDefaultStateBucketName, G as MIGRATE_TMP_PREFIX, H as warnDeprecatedNoPrefixCliFlag, I as getLegacyStateBucketName, J as AssemblyReader, K as findLargeInlineResources, L as resolveApp, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as Synthesizer, Q as CdkdError, R as resolveCaptureObservedState, S as IntrinsicFunctionResolver, T as DagBuilder, U as CFN_TEMPLATE_BODY_LIMIT, V as resolveStateBucketWithDefaultAndSource, W as CFN_TEMPLATE_URL_LIMIT, X as resolveBucketRegion, _ as normalizeAwsTagsToCfn, _t as normalizeAwsError, a as withRetry, at as MissingCdkCliError, b as CloudControlProvider, c as cyan, ct as ResourceTimeoutError, d as red, dt as StackHasActiveImportsError, f as yellow, ft as StackTerminationProtectionError, g as matchesCdkPath, h as CDK_PATH_TAG, i as withResourceDeadline, j as stringifyValue, k as shouldRetainResource, l as gray, lt as ResourceUpdateNotSupportedError, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as LocalMigrateError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as PartialFailureError, p as IAMRoleProvider, q as uploadCfnTemplate, r as DeployEngine, rt as LocalStartServiceError, s as bold, st as ProvisioningError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as LocalInvokeBuildError, u as green, ut as RouteDiscoveryError, v as resolveExplicitPhysicalId, vt as withErrorHandling, w as DiffCalculator, x as assertRegionMatch, y as ProviderRegistry, z as resolveSkipPrefix } from "./deploy-engine-Dff3_JMn.js";
3
+ import { A as AssetPublisher, B as resolveStateBucketWithDefault, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as getDefaultStateBucketName, G as MIGRATE_TMP_PREFIX, H as warnDeprecatedNoPrefixCliFlag, I as getLegacyStateBucketName, J as AssemblyReader, K as findLargeInlineResources, L as resolveApp, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as Synthesizer, Q as CdkdError, R as resolveCaptureObservedState, S as IntrinsicFunctionResolver, T as DagBuilder, U as CFN_TEMPLATE_BODY_LIMIT, V as resolveStateBucketWithDefaultAndSource, W as CFN_TEMPLATE_URL_LIMIT, X as resolveBucketRegion, _ as normalizeAwsTagsToCfn, _t as normalizeAwsError, a as withRetry, at as MissingCdkCliError, b as CloudControlProvider, c as cyan, ct as ResourceTimeoutError, d as red, dt as StackHasActiveImportsError, f as yellow, ft as StackTerminationProtectionError, g as matchesCdkPath, h as CDK_PATH_TAG, i as withResourceDeadline, j as stringifyValue, k as shouldRetainResource, l as gray, lt as ResourceUpdateNotSupportedError, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as LocalMigrateError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as PartialFailureError, p as IAMRoleProvider, q as uploadCfnTemplate, r as DeployEngine, rt as LocalStartServiceError, s as bold, st as ProvisioningError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as LocalInvokeBuildError, u as green, ut as RouteDiscoveryError, v as resolveExplicitPhysicalId, vt as withErrorHandling, w as DiffCalculator, x as assertRegionMatch, y as ProviderRegistry, z as resolveSkipPrefix } from "./deploy-engine-DjnWyAAc.js";
4
4
  import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-BF03Alpe.js";
5
+ import { AsyncLocalStorage } from "node:async_hooks";
5
6
  import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
6
7
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
7
8
  import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachUserPolicyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachUserPolicyCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListInstanceProfilesCommand, ListUserPoliciesCommand, ListUserTagsCommand, ListUsersCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagUserCommand, UntagUserCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
@@ -20,6 +21,7 @@ import { CloudFrontClient, CreateCloudFrontOriginAccessIdentityCommand, CreateDi
20
21
  import { CloudWatchClient, DeleteAlarmsCommand, DescribeAlarmsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$4, PutMetricAlarmCommand, TagResourceCommand as TagResourceCommand$6, UntagResourceCommand as UntagResourceCommand$6 } from "@aws-sdk/client-cloudwatch";
21
22
  import { CloudWatchLogsClient, CreateLogGroupCommand, DeleteDataProtectionPolicyCommand, DeleteIndexPolicyCommand, DeleteLogGroupCommand, DeleteRetentionPolicyCommand, DescribeIndexPoliciesCommand, DescribeLogGroupsCommand, GetDataProtectionPolicyCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$5, PutBearerTokenAuthenticationCommand, PutDataProtectionPolicyCommand, PutIndexPolicyCommand, PutLogGroupDeletionProtectionCommand, PutRetentionPolicyCommand, ResourceAlreadyExistsException, ResourceNotFoundException as ResourceNotFoundException$4, TagResourceCommand as TagResourceCommand$7, UntagResourceCommand as UntagResourceCommand$7 } from "@aws-sdk/client-cloudwatch-logs";
22
23
  import { BedrockAgentCoreControlClient, CreateAgentRuntimeCommand, DeleteAgentRuntimeCommand, GetAgentRuntimeCommand, ResourceNotFoundException as ResourceNotFoundException$5, UpdateAgentRuntimeCommand } from "@aws-sdk/client-bedrock-agentcore-control";
24
+ import * as fs from "node:fs";
23
25
  import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
24
26
  import * as path from "node:path";
25
27
  import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
@@ -27403,7 +27405,17 @@ var EFSProvider = class {
27403
27405
  * Membership grows as each follow-up PR lands. Keep alphabetical
27404
27406
  * to minimize merge conflicts when multiple follow-ups race.
27405
27407
  */
27406
- const SUPPORTED_UPDATE_DESTINATIONS = new Set(["ExtendedS3DestinationConfiguration", "RedshiftDestinationConfiguration"]);
27408
+ const SUPPORTED_UPDATE_DESTINATIONS = new Set([
27409
+ "AmazonOpenSearchServerlessDestinationConfiguration",
27410
+ "AmazonopensearchserviceDestinationConfiguration",
27411
+ "ElasticsearchDestinationConfiguration",
27412
+ "ExtendedS3DestinationConfiguration",
27413
+ "HttpEndpointDestinationConfiguration",
27414
+ "IcebergDestinationConfiguration",
27415
+ "RedshiftDestinationConfiguration",
27416
+ "SnowflakeDestinationConfiguration",
27417
+ "SplunkDestinationConfiguration"
27418
+ ]);
27407
27419
  /**
27408
27420
  * SDK Provider for AWS Kinesis Firehose resources
27409
27421
  *
@@ -27607,18 +27619,85 @@ var FirehoseProvider = class {
27607
27619
  * `CloudWatchLoggingOptions`, `Username` / `Password` /
27608
27620
  * `RoleARN` / `ClusterJDBCURL`.
27609
27621
  *
27610
- * Other destination types (`S3DestinationConfiguration`,
27611
- * `HttpEndpointDestinationConfiguration`,
27612
- * `ElasticsearchDestinationConfiguration`,
27613
- * `AmazonopensearchserviceDestinationConfiguration`,
27614
- * `SplunkDestinationConfiguration`,
27615
- * `AmazonOpenSearchServerlessDestinationConfiguration`,
27616
- * `IcebergDestinationConfiguration`,
27617
- * `SnowflakeDestinationConfiguration`) stay rejected with a tightened
27618
- * error message naming the AWS API. Each one is a follow-up to (#549)
27619
- * — AWS provides `UpdateDestination` for them too, but the per-shape
27620
- * reverse-mappers are deep and each warrants its own focused PR.
27621
- * Re-deploy with `cdkd deploy --replace` until they land.
27622
+ * - `SplunkDestinationConfiguration` (#549) same flow as
27623
+ * Redshift: `DescribeDeliveryStream` recovers VersionId +
27624
+ * DestinationId, then `UpdateDestinationCommand` issues the
27625
+ * diff with a `SplunkDestinationUpdate` payload produced by
27626
+ * {@link mapSplunkConfigToUpdate}. Handles `HECEndpoint`,
27627
+ * `HECEndpointType`, `HECToken`,
27628
+ * `HECAcknowledgmentTimeoutInSeconds`, `RetryOptions`,
27629
+ * `S3BackupMode`, `S3Configuration` `S3Update`,
27630
+ * `ProcessingConfiguration`, `CloudWatchLoggingOptions`,
27631
+ * `BufferingHints`, `SecretsManagerConfiguration`. Splunk has
27632
+ * no S3 backup destination shape (no `S3BackupConfiguration`
27633
+ * field) unlike Redshift / ExtendedS3.
27634
+ *
27635
+ * - `AmazonopensearchserviceDestinationConfiguration` (#549) —
27636
+ * `AmazonopensearchserviceDestinationUpdate` payload produced
27637
+ * by {@link mapAmazonopensearchserviceConfigToUpdate}. Handles
27638
+ * `RoleARN`, `DomainARN`, `ClusterEndpoint`, `IndexName`,
27639
+ * `TypeName`, `IndexRotationPeriod`, `BufferingHints`,
27640
+ * `RetryOptions`, `S3Configuration` → `S3Update`,
27641
+ * `ProcessingConfiguration`, `CloudWatchLoggingOptions`,
27642
+ * `DocumentIdOptions`. `VpcConfiguration` is read-only on
27643
+ * AWS-side Update — diffs to that field will not be forwarded
27644
+ * and surface on the next `cdkd drift` run.
27645
+ *
27646
+ * - `AmazonOpenSearchServerlessDestinationConfiguration` (#549) —
27647
+ * `AmazonOpenSearchServerlessDestinationUpdate` payload
27648
+ * produced by {@link mapAmazonOpenSearchServerlessConfigToUpdate}.
27649
+ * Simpler than the service variant — `RoleARN`,
27650
+ * `CollectionEndpoint`, `IndexName`, `BufferingHints`,
27651
+ * `RetryOptions`, `S3Configuration` → `S3Update`,
27652
+ * `ProcessingConfiguration`, `CloudWatchLoggingOptions`.
27653
+ *
27654
+ * - `HttpEndpointDestinationConfiguration` (#549) —
27655
+ * `HttpEndpointDestinationUpdate` payload produced by
27656
+ * {@link mapHttpEndpointConfigToUpdate}. Handles
27657
+ * `EndpointConfiguration`, `BufferingHints`,
27658
+ * `CloudWatchLoggingOptions`, `RequestConfiguration`,
27659
+ * `ProcessingConfiguration`, `RoleARN`, `RetryOptions`,
27660
+ * `S3BackupMode`, `S3Configuration` → `S3Update`,
27661
+ * `SecretsManagerConfiguration`.
27662
+ *
27663
+ * - `ElasticsearchDestinationConfiguration` (#549) —
27664
+ * `ElasticsearchDestinationUpdate` payload produced by
27665
+ * {@link mapElasticsearchConfigToUpdate}. Legacy variant of
27666
+ * `Amazonopensearchservice*`; same field shape sans
27667
+ * `VpcConfiguration` (also read-only on Update).
27668
+ *
27669
+ * - `IcebergDestinationConfiguration` (#549) —
27670
+ * `IcebergDestinationUpdate` payload produced by
27671
+ * {@link mapIcebergConfigToUpdate}. Handles
27672
+ * `DestinationTableConfigurationList`,
27673
+ * `SchemaEvolutionConfiguration`,
27674
+ * `TableCreationConfiguration`, `BufferingHints`,
27675
+ * `CloudWatchLoggingOptions`, `ProcessingConfiguration`,
27676
+ * `S3BackupMode`, `RetryOptions`, `RoleARN`, `AppendOnly`,
27677
+ * `CatalogConfiguration`, `S3Configuration`. **Quirk**: the
27678
+ * SDK Update shape's S3 field is named `S3Configuration` (full
27679
+ * `S3DestinationConfiguration` shape), NOT `S3Update` — unlike
27680
+ * every other destination type. The reverse-mapper forwards it
27681
+ * verbatim without renaming.
27682
+ *
27683
+ * - `SnowflakeDestinationConfiguration` (#549) —
27684
+ * `SnowflakeDestinationUpdate` payload produced by
27685
+ * {@link mapSnowflakeConfigToUpdate}. Handles many connector
27686
+ * credentials (`AccountUrl` / `PrivateKey` / `KeyPassphrase` /
27687
+ * `User` / `Database` / `Schema` / `Table` /
27688
+ * `SnowflakeRoleConfiguration`) plus the standard suite
27689
+ * (`DataLoadingOption`, `MetaDataColumnName`,
27690
+ * `ContentColumnName`, `CloudWatchLoggingOptions`,
27691
+ * `ProcessingConfiguration`, `RoleARN`, `RetryOptions`,
27692
+ * `S3BackupMode`, `S3Configuration` → `S3Update`,
27693
+ * `SecretsManagerConfiguration`, `BufferingHints`).
27694
+ *
27695
+ * The legacy `S3DestinationConfiguration` (deprecated by AWS in
27696
+ * favor of `ExtendedS3DestinationConfiguration`) stays rejected
27697
+ * with a tightened error — CDK constructs always emit Extended.
27698
+ * Templates that still pin the legacy shape should migrate to
27699
+ * Extended; in-place update of the deprecated shape isn't a
27700
+ * priority follow-up.
27622
27701
  *
27623
27702
  * Destination-type SWITCHES (e.g. ExtendedS3 → Redshift) are immutable
27624
27703
  * on AWS; cdkd surfaces `ResourceUpdateNotSupportedError` so the caller
@@ -27646,6 +27725,41 @@ var FirehoseProvider = class {
27646
27725
  const prevDest = previousProperties[activeDest] ?? {};
27647
27726
  if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applyRedshiftDestinationUpdate(physicalId, nextDest);
27648
27727
  }
27728
+ if (activeDest === "SplunkDestinationConfiguration") {
27729
+ const nextDest = properties[activeDest] ?? {};
27730
+ const prevDest = previousProperties[activeDest] ?? {};
27731
+ if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applySplunkDestinationUpdate(physicalId, nextDest);
27732
+ }
27733
+ if (activeDest === "AmazonopensearchserviceDestinationConfiguration") {
27734
+ const nextDest = properties[activeDest] ?? {};
27735
+ const prevDest = previousProperties[activeDest] ?? {};
27736
+ if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applyAmazonopensearchserviceDestinationUpdate(physicalId, nextDest);
27737
+ }
27738
+ if (activeDest === "AmazonOpenSearchServerlessDestinationConfiguration") {
27739
+ const nextDest = properties[activeDest] ?? {};
27740
+ const prevDest = previousProperties[activeDest] ?? {};
27741
+ if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applyAmazonOpenSearchServerlessDestinationUpdate(physicalId, nextDest);
27742
+ }
27743
+ if (activeDest === "HttpEndpointDestinationConfiguration") {
27744
+ const nextDest = properties[activeDest] ?? {};
27745
+ const prevDest = previousProperties[activeDest] ?? {};
27746
+ if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applyHttpEndpointDestinationUpdate(physicalId, nextDest);
27747
+ }
27748
+ if (activeDest === "ElasticsearchDestinationConfiguration") {
27749
+ const nextDest = properties[activeDest] ?? {};
27750
+ const prevDest = previousProperties[activeDest] ?? {};
27751
+ if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applyElasticsearchDestinationUpdate(physicalId, nextDest);
27752
+ }
27753
+ if (activeDest === "IcebergDestinationConfiguration") {
27754
+ const nextDest = properties[activeDest] ?? {};
27755
+ const prevDest = previousProperties[activeDest] ?? {};
27756
+ if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applyIcebergDestinationUpdate(physicalId, nextDest);
27757
+ }
27758
+ if (activeDest === "SnowflakeDestinationConfiguration") {
27759
+ const nextDest = properties[activeDest] ?? {};
27760
+ const prevDest = previousProperties[activeDest] ?? {};
27761
+ if (JSON.stringify(nextDest) !== JSON.stringify(prevDest)) await this.applySnowflakeDestinationUpdate(physicalId, nextDest);
27762
+ }
27649
27763
  const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
27650
27764
  return {
27651
27765
  physicalId,
@@ -27887,6 +28001,304 @@ var FirehoseProvider = class {
27887
28001
  return result;
27888
28002
  }
27889
28003
  /**
28004
+ * Recover `CurrentDeliveryStreamVersionId` + `DestinationId` from
28005
+ * `DescribeDeliveryStream` and issue `UpdateDestination` with the new
28006
+ * Splunk shape. The reverse-mapper at
28007
+ * {@link mapSplunkConfigToUpdate} produces the
28008
+ * `SplunkDestinationUpdate` payload — only fields present in the
28009
+ * input are forwarded so omitted CFn keys don't clobber AWS-side
28010
+ * state. Mirrors {@link applyRedshiftDestinationUpdate} (#549).
28011
+ */
28012
+ async applySplunkDestinationUpdate(physicalId, nextConfig) {
28013
+ const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
28014
+ const currentVersionId = desc?.VersionId;
28015
+ const destinationId = (desc?.Destinations?.[0])?.DestinationId;
28016
+ if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
28017
+ await this.getClient().send(new UpdateDestinationCommand({
28018
+ DeliveryStreamName: physicalId,
28019
+ CurrentDeliveryStreamVersionId: currentVersionId,
28020
+ DestinationId: destinationId,
28021
+ SplunkDestinationUpdate: this.mapSplunkConfigToUpdate(nextConfig)
28022
+ }));
28023
+ }
28024
+ /**
28025
+ * Map CFn `SplunkDestinationConfiguration` to the
28026
+ * `SplunkDestinationUpdate` shape used by AWS
28027
+ * `UpdateDestinationCommand` (#549). Every field is `!== undefined`
28028
+ * gated so omitted CFn keys do not clobber AWS-side state. The CFn
28029
+ * `S3Configuration` field is mapped through
28030
+ * {@link mapS3ConfigToUpdate} into the SDK-side `S3Update` slot.
28031
+ * Splunk has no `S3BackupConfiguration` field (unlike Redshift /
28032
+ * ExtendedS3).
28033
+ */
28034
+ mapSplunkConfigToUpdate(config) {
28035
+ const result = {};
28036
+ if (config["HECEndpoint"] !== void 0) result.HECEndpoint = config["HECEndpoint"];
28037
+ if (config["HECEndpointType"] !== void 0) result.HECEndpointType = config["HECEndpointType"];
28038
+ if (config["HECToken"] !== void 0) result.HECToken = config["HECToken"];
28039
+ if (config["HECAcknowledgmentTimeoutInSeconds"] !== void 0) result.HECAcknowledgmentTimeoutInSeconds = config["HECAcknowledgmentTimeoutInSeconds"];
28040
+ if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
28041
+ if (config["S3BackupMode"] !== void 0) result.S3BackupMode = config["S3BackupMode"];
28042
+ if (config["S3Configuration"] !== void 0) result.S3Update = this.mapS3ConfigToUpdate(config["S3Configuration"]);
28043
+ if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
28044
+ if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
28045
+ if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
28046
+ if (config["SecretsManagerConfiguration"] !== void 0) result.SecretsManagerConfiguration = config["SecretsManagerConfiguration"];
28047
+ return result;
28048
+ }
28049
+ /**
28050
+ * Apply UpdateDestination for `AmazonopensearchserviceDestinationConfiguration` (#549).
28051
+ * Mirrors {@link applyRedshiftDestinationUpdate}.
28052
+ */
28053
+ async applyAmazonopensearchserviceDestinationUpdate(physicalId, nextConfig) {
28054
+ const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
28055
+ const currentVersionId = desc?.VersionId;
28056
+ const destinationId = desc?.Destinations?.[0]?.DestinationId;
28057
+ if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
28058
+ await this.getClient().send(new UpdateDestinationCommand({
28059
+ DeliveryStreamName: physicalId,
28060
+ CurrentDeliveryStreamVersionId: currentVersionId,
28061
+ DestinationId: destinationId,
28062
+ AmazonopensearchserviceDestinationUpdate: this.mapAmazonopensearchserviceConfigToUpdate(nextConfig)
28063
+ }));
28064
+ }
28065
+ /**
28066
+ * Map CFn `AmazonopensearchserviceDestinationConfiguration` to the
28067
+ * `AmazonopensearchserviceDestinationUpdate` shape (#549). Every field
28068
+ * `!== undefined` gated. CFn `S3Configuration` → SDK `S3Update`.
28069
+ * `VpcConfiguration` is intentionally NOT included — the SDK Update
28070
+ * shape has no field for it (AWS treats Vpc placement as immutable
28071
+ * post-create). VpcConfiguration-only diffs will surface on the next
28072
+ * `cdkd drift` run if the user re-applies a different Vpc on the
28073
+ * console side.
28074
+ */
28075
+ mapAmazonopensearchserviceConfigToUpdate(config) {
28076
+ const result = {};
28077
+ const roleArn = config["RoleARN"] ?? config["RoleArn"];
28078
+ if (roleArn !== void 0) result.RoleARN = roleArn;
28079
+ const domainArn = config["DomainARN"] ?? config["DomainArn"];
28080
+ if (domainArn !== void 0) result.DomainARN = domainArn;
28081
+ if (config["ClusterEndpoint"] !== void 0) result.ClusterEndpoint = config["ClusterEndpoint"];
28082
+ if (config["IndexName"] !== void 0) result.IndexName = config["IndexName"];
28083
+ if (config["TypeName"] !== void 0) result.TypeName = config["TypeName"];
28084
+ if (config["IndexRotationPeriod"] !== void 0) result.IndexRotationPeriod = config["IndexRotationPeriod"];
28085
+ if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
28086
+ if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
28087
+ if (config["S3Configuration"] !== void 0) result.S3Update = this.mapS3ConfigToUpdate(config["S3Configuration"]);
28088
+ if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
28089
+ if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
28090
+ if (config["DocumentIdOptions"] !== void 0) result.DocumentIdOptions = config["DocumentIdOptions"];
28091
+ return result;
28092
+ }
28093
+ /**
28094
+ * Apply UpdateDestination for `AmazonOpenSearchServerlessDestinationConfiguration` (#549).
28095
+ * Mirrors {@link applyRedshiftDestinationUpdate}.
28096
+ */
28097
+ async applyAmazonOpenSearchServerlessDestinationUpdate(physicalId, nextConfig) {
28098
+ const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
28099
+ const currentVersionId = desc?.VersionId;
28100
+ const destinationId = desc?.Destinations?.[0]?.DestinationId;
28101
+ if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
28102
+ await this.getClient().send(new UpdateDestinationCommand({
28103
+ DeliveryStreamName: physicalId,
28104
+ CurrentDeliveryStreamVersionId: currentVersionId,
28105
+ DestinationId: destinationId,
28106
+ AmazonOpenSearchServerlessDestinationUpdate: this.mapAmazonOpenSearchServerlessConfigToUpdate(nextConfig)
28107
+ }));
28108
+ }
28109
+ /**
28110
+ * Map CFn `AmazonOpenSearchServerlessDestinationConfiguration` to the
28111
+ * `AmazonOpenSearchServerlessDestinationUpdate` shape (#549). Every
28112
+ * field `!== undefined` gated. CFn `S3Configuration` → SDK `S3Update`.
28113
+ * Simpler than the service variant — no VpcConfiguration / TypeName /
28114
+ * IndexRotationPeriod / DocumentIdOptions.
28115
+ */
28116
+ mapAmazonOpenSearchServerlessConfigToUpdate(config) {
28117
+ const result = {};
28118
+ const roleArn = config["RoleARN"] ?? config["RoleArn"];
28119
+ if (roleArn !== void 0) result.RoleARN = roleArn;
28120
+ if (config["CollectionEndpoint"] !== void 0) result.CollectionEndpoint = config["CollectionEndpoint"];
28121
+ if (config["IndexName"] !== void 0) result.IndexName = config["IndexName"];
28122
+ if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
28123
+ if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
28124
+ if (config["S3Configuration"] !== void 0) result.S3Update = this.mapS3ConfigToUpdate(config["S3Configuration"]);
28125
+ if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
28126
+ if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
28127
+ return result;
28128
+ }
28129
+ /**
28130
+ * Apply UpdateDestination for `HttpEndpointDestinationConfiguration` (#549).
28131
+ * Mirrors {@link applyRedshiftDestinationUpdate}.
28132
+ */
28133
+ async applyHttpEndpointDestinationUpdate(physicalId, nextConfig) {
28134
+ const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
28135
+ const currentVersionId = desc?.VersionId;
28136
+ const destinationId = desc?.Destinations?.[0]?.DestinationId;
28137
+ if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
28138
+ await this.getClient().send(new UpdateDestinationCommand({
28139
+ DeliveryStreamName: physicalId,
28140
+ CurrentDeliveryStreamVersionId: currentVersionId,
28141
+ DestinationId: destinationId,
28142
+ HttpEndpointDestinationUpdate: this.mapHttpEndpointConfigToUpdate(nextConfig)
28143
+ }));
28144
+ }
28145
+ /**
28146
+ * Map CFn `HttpEndpointDestinationConfiguration` to the
28147
+ * `HttpEndpointDestinationUpdate` shape (#549). Every field
28148
+ * `!== undefined` gated. CFn `S3Configuration` → SDK `S3Update`.
28149
+ * `EndpointConfiguration` (Url / Name / AccessKey) and
28150
+ * `SecretsManagerConfiguration` are both pass-through (AWS accepts
28151
+ * either auth mode on Update).
28152
+ */
28153
+ mapHttpEndpointConfigToUpdate(config) {
28154
+ const result = {};
28155
+ if (config["EndpointConfiguration"] !== void 0) result.EndpointConfiguration = config["EndpointConfiguration"];
28156
+ if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
28157
+ if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
28158
+ if (config["RequestConfiguration"] !== void 0) result.RequestConfiguration = config["RequestConfiguration"];
28159
+ if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
28160
+ const roleArn = config["RoleARN"] ?? config["RoleArn"];
28161
+ if (roleArn !== void 0) result.RoleARN = roleArn;
28162
+ if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
28163
+ if (config["S3BackupMode"] !== void 0) result.S3BackupMode = config["S3BackupMode"];
28164
+ if (config["S3Configuration"] !== void 0) result.S3Update = this.mapS3ConfigToUpdate(config["S3Configuration"]);
28165
+ if (config["SecretsManagerConfiguration"] !== void 0) result.SecretsManagerConfiguration = config["SecretsManagerConfiguration"];
28166
+ return result;
28167
+ }
28168
+ /**
28169
+ * Apply UpdateDestination for `ElasticsearchDestinationConfiguration` (#549).
28170
+ * Mirrors {@link applyRedshiftDestinationUpdate}. Elasticsearch is the
28171
+ * legacy variant — AWS still accepts it for stacks that pre-date the
28172
+ * Amazonopensearchservice rename.
28173
+ */
28174
+ async applyElasticsearchDestinationUpdate(physicalId, nextConfig) {
28175
+ const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
28176
+ const currentVersionId = desc?.VersionId;
28177
+ const destinationId = desc?.Destinations?.[0]?.DestinationId;
28178
+ if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
28179
+ await this.getClient().send(new UpdateDestinationCommand({
28180
+ DeliveryStreamName: physicalId,
28181
+ CurrentDeliveryStreamVersionId: currentVersionId,
28182
+ DestinationId: destinationId,
28183
+ ElasticsearchDestinationUpdate: this.mapElasticsearchConfigToUpdate(nextConfig)
28184
+ }));
28185
+ }
28186
+ /**
28187
+ * Map CFn `ElasticsearchDestinationConfiguration` to the
28188
+ * `ElasticsearchDestinationUpdate` shape (#549). Every field
28189
+ * `!== undefined` gated. CFn `S3Configuration` → SDK `S3Update`.
28190
+ * Field set mirrors {@link mapAmazonopensearchserviceConfigToUpdate}
28191
+ * — Elasticsearch is the legacy variant of the same destination.
28192
+ */
28193
+ mapElasticsearchConfigToUpdate(config) {
28194
+ const result = {};
28195
+ const roleArn = config["RoleARN"] ?? config["RoleArn"];
28196
+ if (roleArn !== void 0) result.RoleARN = roleArn;
28197
+ const domainArn = config["DomainARN"] ?? config["DomainArn"];
28198
+ if (domainArn !== void 0) result.DomainARN = domainArn;
28199
+ if (config["ClusterEndpoint"] !== void 0) result.ClusterEndpoint = config["ClusterEndpoint"];
28200
+ if (config["IndexName"] !== void 0) result.IndexName = config["IndexName"];
28201
+ if (config["TypeName"] !== void 0) result.TypeName = config["TypeName"];
28202
+ if (config["IndexRotationPeriod"] !== void 0) result.IndexRotationPeriod = config["IndexRotationPeriod"];
28203
+ if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
28204
+ if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
28205
+ if (config["S3Configuration"] !== void 0) result.S3Update = this.mapS3ConfigToUpdate(config["S3Configuration"]);
28206
+ if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
28207
+ if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
28208
+ if (config["DocumentIdOptions"] !== void 0) result.DocumentIdOptions = config["DocumentIdOptions"];
28209
+ return result;
28210
+ }
28211
+ /**
28212
+ * Apply UpdateDestination for `IcebergDestinationConfiguration` (#549).
28213
+ * Mirrors {@link applyRedshiftDestinationUpdate}.
28214
+ */
28215
+ async applyIcebergDestinationUpdate(physicalId, nextConfig) {
28216
+ const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
28217
+ const currentVersionId = desc?.VersionId;
28218
+ const destinationId = desc?.Destinations?.[0]?.DestinationId;
28219
+ if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
28220
+ await this.getClient().send(new UpdateDestinationCommand({
28221
+ DeliveryStreamName: physicalId,
28222
+ CurrentDeliveryStreamVersionId: currentVersionId,
28223
+ DestinationId: destinationId,
28224
+ IcebergDestinationUpdate: this.mapIcebergConfigToUpdate(nextConfig)
28225
+ }));
28226
+ }
28227
+ /**
28228
+ * Map CFn `IcebergDestinationConfiguration` to the
28229
+ * `IcebergDestinationUpdate` shape (#549). Every field
28230
+ * `!== undefined` gated. **Quirk vs every other destination**: the
28231
+ * SDK Update shape's S3 field is `S3Configuration` (a full
28232
+ * `S3DestinationConfiguration`), NOT `S3Update`. Iceberg is a newer
28233
+ * destination type and AWS chose to keep the field name aligned with
28234
+ * the create shape; this reverse-mapper forwards it verbatim.
28235
+ */
28236
+ mapIcebergConfigToUpdate(config) {
28237
+ const result = {};
28238
+ if (config["DestinationTableConfigurationList"] !== void 0) result.DestinationTableConfigurationList = config["DestinationTableConfigurationList"];
28239
+ if (config["SchemaEvolutionConfiguration"] !== void 0) result.SchemaEvolutionConfiguration = config["SchemaEvolutionConfiguration"];
28240
+ if (config["TableCreationConfiguration"] !== void 0) result.TableCreationConfiguration = config["TableCreationConfiguration"];
28241
+ if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
28242
+ if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
28243
+ if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
28244
+ if (config["S3BackupMode"] !== void 0) result.S3BackupMode = config["S3BackupMode"];
28245
+ if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
28246
+ const roleArn = config["RoleARN"] ?? config["RoleArn"];
28247
+ if (roleArn !== void 0) result.RoleARN = roleArn;
28248
+ if (config["AppendOnly"] !== void 0) result.AppendOnly = config["AppendOnly"];
28249
+ if (config["CatalogConfiguration"] !== void 0) result.CatalogConfiguration = config["CatalogConfiguration"];
28250
+ if (config["S3Configuration"] !== void 0) result.S3Configuration = this.mapS3DestinationConfiguration(config["S3Configuration"]);
28251
+ return result;
28252
+ }
28253
+ /**
28254
+ * Apply UpdateDestination for `SnowflakeDestinationConfiguration` (#549).
28255
+ * Mirrors {@link applyRedshiftDestinationUpdate}.
28256
+ */
28257
+ async applySnowflakeDestinationUpdate(physicalId, nextConfig) {
28258
+ const desc = (await this.getClient().send(new DescribeDeliveryStreamCommand({ DeliveryStreamName: physicalId }))).DeliveryStreamDescription;
28259
+ const currentVersionId = desc?.VersionId;
28260
+ const destinationId = desc?.Destinations?.[0]?.DestinationId;
28261
+ if (!currentVersionId || !destinationId) throw new ProvisioningError(`DescribeDeliveryStream for ${physicalId} did not return VersionId or DestinationId; UpdateDestination cannot proceed.`, "AWS::KinesisFirehose::DeliveryStream", physicalId);
28262
+ await this.getClient().send(new UpdateDestinationCommand({
28263
+ DeliveryStreamName: physicalId,
28264
+ CurrentDeliveryStreamVersionId: currentVersionId,
28265
+ DestinationId: destinationId,
28266
+ SnowflakeDestinationUpdate: this.mapSnowflakeConfigToUpdate(nextConfig)
28267
+ }));
28268
+ }
28269
+ /**
28270
+ * Map CFn `SnowflakeDestinationConfiguration` to the
28271
+ * `SnowflakeDestinationUpdate` shape (#549). Every field
28272
+ * `!== undefined` gated. CFn `S3Configuration` → SDK `S3Update`.
28273
+ * Snowflake carries many connector-side credential / target-table
28274
+ * fields (`AccountUrl` / `PrivateKey` / `KeyPassphrase` / `User` /
28275
+ * `Database` / `Schema` / `Table`) all pass-through.
28276
+ */
28277
+ mapSnowflakeConfigToUpdate(config) {
28278
+ const result = {};
28279
+ if (config["AccountUrl"] !== void 0) result.AccountUrl = config["AccountUrl"];
28280
+ if (config["PrivateKey"] !== void 0) result.PrivateKey = config["PrivateKey"];
28281
+ if (config["KeyPassphrase"] !== void 0) result.KeyPassphrase = config["KeyPassphrase"];
28282
+ if (config["User"] !== void 0) result.User = config["User"];
28283
+ if (config["Database"] !== void 0) result.Database = config["Database"];
28284
+ if (config["Schema"] !== void 0) result.Schema = config["Schema"];
28285
+ if (config["Table"] !== void 0) result.Table = config["Table"];
28286
+ if (config["SnowflakeRoleConfiguration"] !== void 0) result.SnowflakeRoleConfiguration = config["SnowflakeRoleConfiguration"];
28287
+ if (config["DataLoadingOption"] !== void 0) result.DataLoadingOption = config["DataLoadingOption"];
28288
+ if (config["MetaDataColumnName"] !== void 0) result.MetaDataColumnName = config["MetaDataColumnName"];
28289
+ if (config["ContentColumnName"] !== void 0) result.ContentColumnName = config["ContentColumnName"];
28290
+ if (config["CloudWatchLoggingOptions"] !== void 0) result.CloudWatchLoggingOptions = config["CloudWatchLoggingOptions"];
28291
+ if (config["ProcessingConfiguration"] !== void 0) result.ProcessingConfiguration = config["ProcessingConfiguration"];
28292
+ const roleArn = config["RoleARN"] ?? config["RoleArn"];
28293
+ if (roleArn !== void 0) result.RoleARN = roleArn;
28294
+ if (config["RetryOptions"] !== void 0) result.RetryOptions = config["RetryOptions"];
28295
+ if (config["S3BackupMode"] !== void 0) result.S3BackupMode = config["S3BackupMode"];
28296
+ if (config["S3Configuration"] !== void 0) result.S3Update = this.mapS3ConfigToUpdate(config["S3Configuration"]);
28297
+ if (config["SecretsManagerConfiguration"] !== void 0) result.SecretsManagerConfiguration = config["SecretsManagerConfiguration"];
28298
+ if (config["BufferingHints"] !== void 0) result.BufferingHints = config["BufferingHints"];
28299
+ return result;
28300
+ }
28301
+ /**
27890
28302
  * Map CFn `S3DestinationConfiguration` to the `S3DestinationUpdate`
27891
28303
  * shape used by AWS `UpdateDestinationCommand` (#477; consumed by
27892
28304
  * {@link mapExtendedS3ConfigToUpdate} for the `S3BackupUpdate` field).
@@ -30944,6 +31356,602 @@ function mapNotificationsToCfn(configurations) {
30944
31356
  return result;
30945
31357
  }
30946
31358
 
31359
+ //#endregion
31360
+ //#region src/cli/commands/destroy-runner.ts
31361
+ /**
31362
+ * Resource-type → state-property name pairs that gate AWS deletion
31363
+ * protection. Used by the `--remove-protection` confirmation prompt to
31364
+ * report a best-effort count of resources that will have protection
31365
+ * cleared. The actual flip-off is unconditional inside each provider's
31366
+ * `delete()` (idempotent — safe when AWS already has protection off),
31367
+ * so the count is informational only.
31368
+ *
31369
+ * Most types use a boolean flag — the value `true` is what we count.
31370
+ * Two types use a string-valued enum (Cognito UserPool's
31371
+ * `DeletionProtection` is `'ACTIVE' | 'INACTIVE'`, AutoScalingGroup's
31372
+ * `DeletionProtection` is `'none' | 'prevent-force-deletion' |
31373
+ * 'prevent-all-deletion'`). For those, the helper checks against a
31374
+ * per-type set of "active" values via `PROTECTION_ACTIVE_VALUES_BY_TYPE`.
31375
+ *
31376
+ * Exported for unit-test coverage of `countProtectedResources`.
31377
+ */
31378
+ const PROTECTION_PROPERTY_BY_TYPE = {
31379
+ "AWS::Logs::LogGroup": "DeletionProtectionEnabled",
31380
+ "AWS::RDS::DBInstance": "DeletionProtection",
31381
+ "AWS::RDS::DBCluster": "DeletionProtection",
31382
+ "AWS::DocDB::DBCluster": "DeletionProtection",
31383
+ "AWS::Neptune::DBCluster": "DeletionProtection",
31384
+ "AWS::Neptune::DBInstance": "DeletionProtection",
31385
+ "AWS::DynamoDB::Table": "DeletionProtectionEnabled",
31386
+ "AWS::DynamoDB::GlobalTable": "DeletionProtectionEnabled",
31387
+ "AWS::EC2::Instance": "DisableApiTermination",
31388
+ "AWS::Cognito::UserPool": "DeletionProtection",
31389
+ "AWS::AutoScaling::AutoScalingGroup": "DeletionProtection"
31390
+ };
31391
+ /**
31392
+ * For string-valued protection enums, the set of values that count as
31393
+ * "currently protected". Types absent from this map use the default
31394
+ * (boolean `true`).
31395
+ */
31396
+ const PROTECTION_ACTIVE_VALUES_BY_TYPE = {
31397
+ "AWS::Cognito::UserPool": new Set(["ACTIVE"]),
31398
+ "AWS::AutoScaling::AutoScalingGroup": new Set(["prevent-force-deletion", "prevent-all-deletion"])
31399
+ };
31400
+ /**
31401
+ * Count how many resources in a stack's recorded state appear to have
31402
+ * deletion protection enabled. Walks `properties` and `observedProperties`
31403
+ * for the property name registered against each resource type in
31404
+ * `PROTECTION_PROPERTY_BY_TYPE`. ELBv2 LoadBalancer protection lives in
31405
+ * `LoadBalancerAttributes` (a CFn `Array<{Key, Value}>`), so it's
31406
+ * handled separately via the `deletion_protection.enabled` key.
31407
+ */
31408
+ function countProtectedResources(state) {
31409
+ let count = 0;
31410
+ for (const resource of Object.values(state.resources ?? {})) {
31411
+ const propName = PROTECTION_PROPERTY_BY_TYPE[resource.resourceType];
31412
+ if (propName) {
31413
+ const recorded = resource.properties?.[propName] ?? resource.observedProperties?.[propName];
31414
+ const activeValues = PROTECTION_ACTIVE_VALUES_BY_TYPE[resource.resourceType];
31415
+ if (activeValues) {
31416
+ if (activeValues.has(recorded)) count++;
31417
+ } else if (recorded === true) count++;
31418
+ continue;
31419
+ }
31420
+ if (resource.resourceType === "AWS::ElasticLoadBalancingV2::LoadBalancer") {
31421
+ if (((resource.properties?.["LoadBalancerAttributes"] ?? resource.observedProperties?.["LoadBalancerAttributes"])?.find((a) => a?.Key === "deletion_protection.enabled"))?.Value === "true") count++;
31422
+ }
31423
+ }
31424
+ return count;
31425
+ }
31426
+ /**
31427
+ * Run the destroy lifecycle for one stack against an already-loaded
31428
+ * `StackState`, reusing the caller's state backend / lock manager.
31429
+ *
31430
+ * Hoisted from `cdkd destroy` so the new `cdkd state destroy` subcommand
31431
+ * can call into the exact same per-stack pipeline without depending on
31432
+ * synth or the CDK app. The state-source split is the only meaningful
31433
+ * difference between the two commands — everything from "prompt the user"
31434
+ * onwards is identical.
31435
+ *
31436
+ * Side effects:
31437
+ * - Acquires (and releases) the stack's S3 lock.
31438
+ * - Switches `process.env.AWS_REGION` for the duration of the destroy when
31439
+ * the stack's recorded region differs from `baseRegion`. Restored in the
31440
+ * `finally` block.
31441
+ * - On full success, deletes the state file. On any failure, the state
31442
+ * file is preserved so the user can retry.
31443
+ */
31444
+ async function runDestroyForStack(stackName, state, ctx) {
31445
+ const logger = getLogger();
31446
+ const result = {
31447
+ stackName,
31448
+ cancelled: false,
31449
+ skippedEmpty: false,
31450
+ deletedCount: 0,
31451
+ retainedCount: 0,
31452
+ errorCount: 0
31453
+ };
31454
+ const resourceCount = Object.keys(state.resources).length;
31455
+ const regionForState = state.region ?? ctx.baseRegion;
31456
+ if (resourceCount === 0) {
31457
+ logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
31458
+ await ctx.stateBackend.deleteState(stackName, regionForState);
31459
+ logger.info(`${green("✓")} State deleted`);
31460
+ result.skippedEmpty = true;
31461
+ return result;
31462
+ }
31463
+ const needsStrongRefCheck = !!(state.outputs && Object.keys(state.outputs).length > 0);
31464
+ if (needsStrongRefCheck) {
31465
+ const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
31466
+ if (consumers.length > 0) throw new StackHasActiveImportsError(stackName, regionForState, consumers);
31467
+ }
31468
+ logger.info(`\nResources to be deleted (${resourceCount}):`);
31469
+ for (const [logicalId, resource] of Object.entries(state.resources)) logger.info(` - ${logicalId} (${resource.resourceType})`);
31470
+ const protectedCount = ctx.removeProtection ? countProtectedResources(state) : 0;
31471
+ if (!ctx.skipConfirmation) {
31472
+ const rl = readline.createInterface({
31473
+ input: process.stdin,
31474
+ output: process.stdout
31475
+ });
31476
+ const prompt = ctx.removeProtection ? `\nAbout to destroy ${resourceCount} resources from stack "${stackName}", REMOVING DELETION PROTECTION on ${protectedCount} of them. Continue? (y/N): ` : `\nAre you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `;
31477
+ const answer = await rl.question(prompt);
31478
+ rl.close();
31479
+ const trimmed = answer.trim().toLowerCase();
31480
+ if (ctx.removeProtection) {
31481
+ if (trimmed !== "y" && trimmed !== "yes") {
31482
+ logger.info("Destroy cancelled");
31483
+ result.cancelled = true;
31484
+ return result;
31485
+ }
31486
+ } else if (trimmed === "n" || trimmed === "no") {
31487
+ logger.info("Destroy cancelled");
31488
+ result.cancelled = true;
31489
+ return result;
31490
+ }
31491
+ }
31492
+ const stackRegion = state.region;
31493
+ let destroyProviderRegistry = ctx.providerRegistry;
31494
+ let destroyAwsClients;
31495
+ if (stackRegion && stackRegion !== ctx.baseRegion) {
31496
+ logger.info(`Stack region: ${stackRegion}`);
31497
+ process.env["AWS_REGION"] = stackRegion;
31498
+ process.env["AWS_DEFAULT_REGION"] = stackRegion;
31499
+ destroyAwsClients = new AwsClients({
31500
+ region: stackRegion,
31501
+ ...ctx.profile && { profile: ctx.profile }
31502
+ });
31503
+ setAwsClients(destroyAwsClients);
31504
+ destroyProviderRegistry = new ProviderRegistry();
31505
+ registerAllProviders(destroyProviderRegistry);
31506
+ destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
31507
+ }
31508
+ logger.info(`\nAcquiring lock for stack ${stackName}...`);
31509
+ await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
31510
+ if (needsStrongRefCheck) {
31511
+ const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
31512
+ if (consumers.length > 0) {
31513
+ try {
31514
+ await ctx.lockManager.releaseLock(stackName, regionForState);
31515
+ } catch (releaseErr) {
31516
+ logger.warn(`Failed to release lock after strong-ref refusal: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
31517
+ }
31518
+ throw new StackHasActiveImportsError(stackName, regionForState, consumers);
31519
+ }
31520
+ }
31521
+ const renderer = getLiveRenderer();
31522
+ renderer.start();
31523
+ try {
31524
+ logger.info("Building dependency graph...");
31525
+ const template = {
31526
+ AWSTemplateFormatVersion: "2010-09-09",
31527
+ Resources: {}
31528
+ };
31529
+ for (const [logicalId, resource] of Object.entries(state.resources)) template.Resources[logicalId] = {
31530
+ Type: resource.resourceType,
31531
+ Properties: resource.properties || {},
31532
+ ...resource.dependencies && resource.dependencies.length > 0 && { DependsOn: resource.dependencies }
31533
+ };
31534
+ const typeToLogicalIds = /* @__PURE__ */ new Map();
31535
+ for (const [logicalId, resource] of Object.entries(state.resources)) {
31536
+ const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
31537
+ ids.push(logicalId);
31538
+ typeToLogicalIds.set(resource.resourceType, ids);
31539
+ }
31540
+ for (const [logicalId, resource] of Object.entries(state.resources)) {
31541
+ const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
31542
+ if (!mustDeleteAfter) continue;
31543
+ for (const depType of mustDeleteAfter) {
31544
+ const depIds = typeToLogicalIds.get(depType);
31545
+ if (!depIds) continue;
31546
+ for (const depId of depIds) {
31547
+ const existing = template.Resources[depId]?.DependsOn ?? [];
31548
+ const depsArray = Array.isArray(existing) ? existing : [existing];
31549
+ if (!depsArray.includes(logicalId)) {
31550
+ template.Resources[depId] = {
31551
+ ...template.Resources[depId],
31552
+ DependsOn: [...depsArray, logicalId]
31553
+ };
31554
+ logger.debug(`Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`);
31555
+ }
31556
+ }
31557
+ }
31558
+ }
31559
+ const dagBuilder = new DagBuilder();
31560
+ const graph = dagBuilder.buildGraph(template);
31561
+ const executionLevels = dagBuilder.getExecutionLevels(graph);
31562
+ logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
31563
+ for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
31564
+ const level = executionLevels[levelIndex];
31565
+ if (!level) continue;
31566
+ logger.debug(`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`);
31567
+ const stackRegion = state.region ?? ctx.baseRegion;
31568
+ const deletePromises = level.map(async (logicalId) => {
31569
+ const resource = state.resources[logicalId];
31570
+ if (!resource) {
31571
+ logger.warn(`Resource ${logicalId} not found in state, skipping`);
31572
+ return;
31573
+ }
31574
+ if (shouldRetainResource(resource.deletionPolicy)) {
31575
+ logger.info(` ⊘ ${logicalId} (${resource.resourceType}) retained — DeletionPolicy: ${resource.deletionPolicy}`);
31576
+ result.retainedCount++;
31577
+ return;
31578
+ }
31579
+ const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
31580
+ renderer.addTask(logicalId, baseLabel);
31581
+ try {
31582
+ const provider = destroyProviderRegistry.getProvider(resource.resourceType);
31583
+ const providerMinTimeoutMs = provider.getMinResourceTimeoutMs?.() ?? 0;
31584
+ const warnAfterMs = ctx.resourceWarnAfterByType?.[resource.resourceType] ?? ctx.resourceWarnAfterMs ?? 3e5;
31585
+ const globalTimeoutMs = ctx.resourceTimeoutMs ?? 18e5;
31586
+ const timeoutMs = ctx.resourceTimeoutByType?.[resource.resourceType] ?? Math.max(providerMinTimeoutMs, globalTimeoutMs);
31587
+ await withResourceDeadline(async () => {
31588
+ const maxAttempts = provider.disableOuterRetry ? 0 : 3;
31589
+ let lastDeleteError;
31590
+ for (let attempt = 0; attempt <= maxAttempts; attempt++) try {
31591
+ await provider.delete(logicalId, resource.physicalId, resource.resourceType, resource.properties, {
31592
+ ...state.region !== void 0 && { expectedRegion: state.region },
31593
+ ...ctx.removeProtection === true && { removeProtection: true }
31594
+ });
31595
+ lastDeleteError = null;
31596
+ break;
31597
+ } catch (retryError) {
31598
+ lastDeleteError = retryError;
31599
+ const msg = retryError instanceof Error ? retryError.message : String(retryError);
31600
+ if (!(msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation")) || attempt >= maxAttempts) break;
31601
+ const delay = 5e3 * Math.pow(2, attempt);
31602
+ logger.debug(` ⏳ Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxAttempts})`);
31603
+ await new Promise((resolve) => setTimeout(resolve, delay));
31604
+ }
31605
+ if (lastDeleteError) throw lastDeleteError;
31606
+ }, {
31607
+ warnAfterMs,
31608
+ timeoutMs,
31609
+ onWarn: (elapsedMs) => {
31610
+ const minutes = Math.max(1, Math.round(elapsedMs / 6e4));
31611
+ renderer.updateTaskLabel(logicalId, `${baseLabel} [taking longer than expected, ${minutes}m+]`);
31612
+ renderer.printAbove(() => {
31613
+ logger.warn(`${logicalId} (${resource.resourceType}) has been deleting for ${minutes}m — still waiting`);
31614
+ });
31615
+ },
31616
+ onTimeout: (elapsedMs) => new ResourceTimeoutError(logicalId, resource.resourceType, stackRegion, elapsedMs, "DELETE", timeoutMs)
31617
+ });
31618
+ renderer.removeTask(logicalId);
31619
+ logger.info(` ${red("✗")} ${bold(logicalId)} ${gray(`(${resource.resourceType})`)} ${red("deleted")}`);
31620
+ result.deletedCount++;
31621
+ } catch (error) {
31622
+ renderer.removeTask(logicalId);
31623
+ const msg = error instanceof Error ? error.message : String(error);
31624
+ if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
31625
+ logger.debug(` ${logicalId} already deleted, removing from state`);
31626
+ result.deletedCount++;
31627
+ } else if (error instanceof ResourceTimeoutError) {
31628
+ const wrapped = new ProvisioningError(error.message, resource.resourceType, logicalId, resource.physicalId, error);
31629
+ logger.error(` ✗ Failed to delete ${logicalId}:`, wrapped.message);
31630
+ result.errorCount++;
31631
+ } else {
31632
+ logger.error(` ✗ Failed to delete ${logicalId}:`, String(error));
31633
+ result.errorCount++;
31634
+ }
31635
+ } finally {
31636
+ renderer.removeTask(logicalId);
31637
+ }
31638
+ });
31639
+ await Promise.all(deletePromises);
31640
+ }
31641
+ if (result.errorCount === 0) {
31642
+ await ctx.stateBackend.deleteState(stackName, regionForState);
31643
+ logger.debug("State deleted");
31644
+ if (ctx.exportIndexStore) await ctx.exportIndexStore.removeStack(stackName, regionForState);
31645
+ } else logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
31646
+ const retainedSuffix = result.retainedCount > 0 ? `, ${result.retainedCount} retained` : "";
31647
+ if (result.errorCount === 0) logger.info(`\n${green("✓")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
31648
+ else logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} partially destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${red(result.errorCount)} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to clean up.`);
31649
+ } finally {
31650
+ renderer.stop();
31651
+ logger.debug("Releasing lock...");
31652
+ await ctx.lockManager.releaseLock(stackName, regionForState);
31653
+ if (destroyAwsClients) {
31654
+ destroyAwsClients.destroy();
31655
+ process.env["AWS_REGION"] = ctx.baseRegion;
31656
+ process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
31657
+ setAwsClients(ctx.baseAwsClients);
31658
+ }
31659
+ }
31660
+ return result;
31661
+ }
31662
+ /**
31663
+ * Strong-reference scan: read every other stack's state.json from the
31664
+ * state bucket and check whether any of its `imports[]` entries names
31665
+ * `producerStack`. Returns the list of offending consumers (possibly
31666
+ * empty).
31667
+ *
31668
+ * NEVER trusts the persistent exports index — a stale index could miss
31669
+ * a freshly-recorded consumer and let a destructive destroy through.
31670
+ * The cost is one `listStacks` + N parallel GETs at destroy time only
31671
+ * (not the deploy hot path), which the user-facing UX rationalizes as
31672
+ * the "destroy is slow OK" trade-off (Issue #343).
31673
+ */
31674
+ async function scanActiveConsumers(producerStack, producerRegion, ctx) {
31675
+ const refs = await ctx.stateBackend.listStacks();
31676
+ return (await Promise.all(refs.map(async (ref) => {
31677
+ const region = ref.region ?? ctx.baseRegion;
31678
+ if (ref.stackName === producerStack && region === producerRegion) return null;
31679
+ try {
31680
+ const imports = (await ctx.stateBackend.getState(ref.stackName, region))?.state.imports;
31681
+ if (!imports || imports.length === 0) return null;
31682
+ const matches = imports.filter((entry) => entry.sourceStack === producerStack && entry.sourceRegion === producerRegion);
31683
+ if (matches.length === 0) return null;
31684
+ return matches.map((entry) => ({
31685
+ consumerStack: ref.stackName,
31686
+ consumerRegion: region,
31687
+ exportName: entry.exportName
31688
+ }));
31689
+ } catch {
31690
+ return null;
31691
+ }
31692
+ }))).filter((r) => r !== null).flat();
31693
+ }
31694
+
31695
+ //#endregion
31696
+ //#region src/provisioning/nested-stack-context.ts
31697
+ const storage = new AsyncLocalStorage();
31698
+ /**
31699
+ * Run `fn` inside a NestedStackProvider context scope. Calls to
31700
+ * `getCurrentNestedStackContext()` from inside `fn` (and any awaited callees)
31701
+ * return `ctx`. Nested scopes shadow outer ones — the recursive provider
31702
+ * uses this to switch the "current parent" to the child before kicking off
31703
+ * the child's deploy / destroy, so grand-nested handling resolves against
31704
+ * the right parent.
31705
+ */
31706
+ function withNestedStackContext(ctx, fn) {
31707
+ return storage.run(ctx, fn);
31708
+ }
31709
+ /**
31710
+ * Returns the current `NestedStackProviderContext`, or `undefined` when called
31711
+ * outside any `withNestedStackContext` scope (= cdkd is operating on a
31712
+ * top-level stack with no nested-stack work in flight).
31713
+ *
31714
+ * `NestedStackProvider.create / update / delete` MUST find a context here —
31715
+ * absence means a caller forgot to wrap the deploy / destroy entry point.
31716
+ */
31717
+ function getCurrentNestedStackContext() {
31718
+ return storage.getStore();
31719
+ }
31720
+
31721
+ //#endregion
31722
+ //#region src/provisioning/providers/nested-stack-provider.ts
31723
+ /**
31724
+ * Provider for `AWS::CloudFormation::Stack` — cdkd's recursive nested-stack
31725
+ * adapter. Issue [#459](https://github.com/go-to-k/cdkd/issues/459); see
31726
+ * [docs/design/459-nested-stacks.md](../../../docs/design/459-nested-stacks.md)
31727
+ * for the full design.
31728
+ *
31729
+ * On `create` / `update`, the provider builds a child {@link DeployEngine}
31730
+ * against the same shared state backend / lock manager / provider registry,
31731
+ * deploys the child template recursively, and surfaces the child's outputs
31732
+ * as `attributes['Outputs.<Key>']` so the parent's
31733
+ * `Fn::GetAtt: [<NestedStack>, 'Outputs.<Key>']` references resolve via
31734
+ * the existing flat-key fast path in {@link IntrinsicFunctionResolver}.
31735
+ *
31736
+ * On `delete`, the provider loads the child's state and routes it through
31737
+ * {@link runDestroyForStack} for a regular reverse-DAG destroy — the same
31738
+ * code `cdkd destroy` uses on a top-level stack.
31739
+ *
31740
+ * The child's state file lives at
31741
+ * `cdkd/<parentStackName>~<NestedStackLogicalId>/<region>/state.json`
31742
+ * (the `~` separator is rare in CDK logical ids; verified safe against
31743
+ * CDK Stage paths which use `/`). The synthesized `physicalId` is a fake
31744
+ * ARN with `cdkd-local` partition so any downstream consumer that
31745
+ * accidentally uses it as a real AWS ARN fails loudly.
31746
+ */
31747
+ var NestedStackProvider = class {
31748
+ logger = getLogger().child("NestedStackProvider");
31749
+ /**
31750
+ * Opt out of the deploy engine's outer transient-error retry loop. A
31751
+ * nested-stack `create` recursively spawns a child {@link DeployEngine}
31752
+ * that has its own retry / rollback machinery; an outer retry would
31753
+ * re-enter the entire child deploy on a transient error and produce
31754
+ * duplicate AWS resources before the second attempt's per-resource
31755
+ * state save settles. The child engine handles transient errors
31756
+ * internally — mirroring the same opt-out the Custom Resource provider
31757
+ * uses for the same reason.
31758
+ */
31759
+ disableOuterRetry = true;
31760
+ /**
31761
+ * The CC API fallback path for `AWS::CloudFormation::Stack` would call
31762
+ * CloudFormation's own `CreateStack` — defeating cdkd's whole "no CFn"
31763
+ * approach for the nested children. Refuse the fallback so any future
31764
+ * regression that drops a real property from `handledProperties`
31765
+ * surfaces as an explicit "unhandled property" deploy error instead of
31766
+ * silently round-tripping through CloudFormation.
31767
+ */
31768
+ disableCcApiFallback = true;
31769
+ /**
31770
+ * Properties this provider actually wires through to the child deploy.
31771
+ * `TemplateURL` is the asset-published S3 URL of the child template
31772
+ * (cdkd reads the local template file via `Metadata['aws:asset:path']`,
31773
+ * so the URL itself is informational here); `Parameters` is the typed
31774
+ * parameter map forwarded as `DeployEngineOptions.parameters` to the
31775
+ * child engine.
31776
+ */
31777
+ handledProperties = new Map([["AWS::CloudFormation::Stack", new Set(["TemplateURL", "Parameters"])]]);
31778
+ /**
31779
+ * Every other property on `AWS::CloudFormation::Stack` is intentionally
31780
+ * not threaded through — cdkd does not go through CloudFormation, so
31781
+ * CFn-only inputs (rollback / capability / role / notification /
31782
+ * termination-protection / stack-update policy / per-stack timeout /
31783
+ * tags) have no equivalent. The synthesized `Ref` ARN is a placeholder,
31784
+ * not a real AWS resource — so `Tags` and `Description` similarly
31785
+ * have nothing to attach to. `StackName` is replaced by cdkd's derived
31786
+ * `<parent>~<logicalId>` key per design §3, and `TemplateBody` is
31787
+ * superseded by the local `Metadata['aws:asset:path']` lookup.
31788
+ */
31789
+ unhandledByDesign = new Map([["AWS::CloudFormation::Stack", new Map([
31790
+ ["TemplateBody", "CFn-only inline template — cdkd reads the child template from the synth output via Metadata['aws:asset:path'] instead of accepting it inline"],
31791
+ ["Capabilities", "CFn-only IAM capability declaration — cdkd does not go through CloudFormation so capabilities have no equivalent"],
31792
+ ["Description", "CFn-only informational — no semantic effect on the recursive deploy"],
31793
+ ["DisableRollback", "CFn-only — cdkd controls rollback via the top-level deploy-engine --no-rollback flag, not per nested stack"],
31794
+ ["EnableTerminationProtection", "CFn-only per-nested-stack flag — cdkd records stack-level terminationProtection at CDK synth time (parent only) and `cdkd destroy` consults that for refusal"],
31795
+ ["NotificationARNs", "CFn-only SNS-on-stack-event surface — cdkd has no equivalent (issue #459 design §9)"],
31796
+ ["RoleARN", "CFn-only role-assumption — cdkd uses the caller credentials directly, no per-resource role assumption"],
31797
+ ["StackName", "cdkd derives the child stack name as `<parent>~<logicalId>` per design §3 (state-key uniqueness); a user-provided StackName has no effect"],
31798
+ ["StackPolicyBody", "CFn-only stack-update policy — cdkd has no equivalent (per-resource diff replaces stack-level policy)"],
31799
+ ["StackPolicyURL", "CFn-only stack-update policy URL — cdkd has no equivalent"],
31800
+ ["StackStatusReason", "CFn-only read-only output — never a real input property"],
31801
+ ["Tags", "CFn-only — cdkd does not tag the synthesized \"stack\" (the parent's synthesized ARN is a cdkd-local placeholder, not a real AWS resource)"],
31802
+ ["TimeoutInMinutes", "CFn-only stack-create deadline — cdkd uses per-resource --resource-timeout instead (issue #459 design §9)"]
31803
+ ])]]);
31804
+ async create(logicalId, _resourceType, properties) {
31805
+ const ctx = this.requireContext();
31806
+ this.requireDeployContext(ctx, "create");
31807
+ const childTemplatePath = ctx.nestedTemplates[logicalId];
31808
+ if (!childTemplatePath) throw new Error(`Nested template file not found for AWS::CloudFormation::Stack '${logicalId}' under parent '${ctx.parentStackName}'. Verify the synth output emits Metadata['aws:asset:path'] on this resource (CDK 2.x cdk.NestedStack does so by default).`);
31809
+ const childTemplate = this.readChildTemplate(childTemplatePath);
31810
+ const childStackName = this.deriveChildStackName(ctx.parentStackName, logicalId);
31811
+ const childRegion = ctx.parentRegion;
31812
+ const childParameters = this.extractParameters(properties);
31813
+ const grandchildTemplates = this.indexGrandchildTemplates(childTemplate, childTemplatePath);
31814
+ const resourceCount = Object.keys(childTemplate.Resources ?? {}).length;
31815
+ this.logger.info(`Deploying nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
31816
+ await this.runChildDeploy(ctx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates);
31817
+ const attributes = await this.readChildOutputsAsAttributes(ctx, childStackName, childRegion);
31818
+ return {
31819
+ physicalId: this.synthesizeArn(ctx.accountId, ctx.parentRegion, ctx.parentStackName, logicalId),
31820
+ attributes
31821
+ };
31822
+ }
31823
+ async update(logicalId, physicalId, _resourceType, properties, _previousProperties) {
31824
+ const ctx = this.requireContext();
31825
+ this.requireDeployContext(ctx, "update");
31826
+ const childTemplatePath = ctx.nestedTemplates[logicalId];
31827
+ if (!childTemplatePath) throw new Error(`Nested template file not found for AWS::CloudFormation::Stack '${logicalId}' on update.`);
31828
+ const childTemplate = this.readChildTemplate(childTemplatePath);
31829
+ const childStackName = this.deriveChildStackName(ctx.parentStackName, logicalId);
31830
+ const childRegion = ctx.parentRegion;
31831
+ const childParameters = this.extractParameters(properties);
31832
+ const grandchildTemplates = this.indexGrandchildTemplates(childTemplate, childTemplatePath);
31833
+ const resourceCount = Object.keys(childTemplate.Resources ?? {}).length;
31834
+ this.logger.info(`Updating nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
31835
+ await this.runChildDeploy(ctx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates);
31836
+ return {
31837
+ physicalId,
31838
+ wasReplaced: false,
31839
+ attributes: await this.readChildOutputsAsAttributes(ctx, childStackName, childRegion)
31840
+ };
31841
+ }
31842
+ async delete(logicalId, _physicalId, _resourceType, _properties, deleteContext) {
31843
+ const ctx = this.requireContext();
31844
+ const childStackName = this.deriveChildStackName(ctx.parentStackName, logicalId);
31845
+ const childRegion = ctx.parentRegion;
31846
+ const childStateData = await ctx.stateBackend.getState(childStackName, childRegion);
31847
+ if (!childStateData) {
31848
+ this.logger.debug(`Nested stack ${childStackName} has no state — treating delete as idempotent success.`);
31849
+ return;
31850
+ }
31851
+ const resourceCount = Object.keys(childStateData.state.resources).length;
31852
+ this.logger.info(`Destroying nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
31853
+ await withNestedStackContext({
31854
+ ...ctx,
31855
+ parentStackName: childStackName,
31856
+ parentRegion: childRegion,
31857
+ nestedTemplates: void 0
31858
+ }, () => runDestroyForStack(childStackName, childStateData.state, {
31859
+ stateBackend: ctx.stateBackend,
31860
+ lockManager: ctx.lockManager,
31861
+ providerRegistry: ctx.providerRegistry,
31862
+ baseAwsClients: ctx.awsClients,
31863
+ baseRegion: childRegion,
31864
+ stateBucket: ctx.stateBucket,
31865
+ skipConfirmation: true,
31866
+ ...ctx.exportIndexStore && { exportIndexStore: ctx.exportIndexStore },
31867
+ ...ctx.destroyOptions?.profile && { profile: ctx.destroyOptions.profile },
31868
+ ...deleteContext?.removeProtection === true && { removeProtection: true },
31869
+ ...ctx.destroyOptions?.resourceWarnAfterMs !== void 0 && { resourceWarnAfterMs: ctx.destroyOptions.resourceWarnAfterMs },
31870
+ ...ctx.destroyOptions?.resourceTimeoutMs !== void 0 && { resourceTimeoutMs: ctx.destroyOptions.resourceTimeoutMs },
31871
+ ...ctx.destroyOptions?.resourceWarnAfterByType && { resourceWarnAfterByType: ctx.destroyOptions.resourceWarnAfterByType },
31872
+ ...ctx.destroyOptions?.resourceTimeoutByType && { resourceTimeoutByType: ctx.destroyOptions.resourceTimeoutByType }
31873
+ }));
31874
+ }
31875
+ async getAttribute(_physicalId, _resourceType, attributeName) {
31876
+ throw new Error(`AWS::CloudFormation::Stack: attribute '${attributeName}' is not in the recorded Outputs map. Only 'Outputs.<Key>' references to declared Output names on the child template are supported.`);
31877
+ }
31878
+ async runChildDeploy(parentCtx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates) {
31879
+ const childEngine = new DeployEngine(parentCtx.stateBackend, parentCtx.lockManager, parentCtx.dagBuilder, parentCtx.diffCalculator, parentCtx.providerRegistry, {
31880
+ ...parentCtx.options ?? {},
31881
+ parameters: childParameters,
31882
+ parentStackInfo: {
31883
+ parentStack: parentCtx.parentStackName,
31884
+ parentLogicalId: logicalId,
31885
+ parentRegion: parentCtx.parentRegion
31886
+ }
31887
+ }, childRegion, parentCtx.exportIndexStore);
31888
+ await withNestedStackContext({
31889
+ ...parentCtx,
31890
+ parentStackName: childStackName,
31891
+ parentRegion: childRegion,
31892
+ nestedTemplates: grandchildTemplates
31893
+ }, () => childEngine.deploy(childStackName, childTemplate));
31894
+ }
31895
+ async readChildOutputsAsAttributes(ctx, childStackName, childRegion) {
31896
+ const childStateData = await ctx.stateBackend.getState(childStackName, childRegion);
31897
+ if (!childStateData) throw new Error(`Child stack state '${childStackName}' not found after deploy — NestedStackProvider invariant violated.`);
31898
+ return this.buildOutputsAttributes(childStateData.state.outputs ?? {});
31899
+ }
31900
+ requireContext() {
31901
+ const ctx = getCurrentNestedStackContext();
31902
+ if (!ctx) throw new Error("NestedStackProvider invoked outside withNestedStackContext() scope. The deploy / destroy CLI entry point must wrap its DeployEngine.deploy / runDestroyForStack call in withNestedStackContext(ctx, () => ...).");
31903
+ return ctx;
31904
+ }
31905
+ requireDeployContext(ctx, op) {
31906
+ if (!ctx.nestedTemplates || !ctx.dagBuilder || !ctx.diffCalculator) throw new Error(`NestedStackProvider.${op}: deploy-mode context fields (nestedTemplates / dagBuilder / diffCalculator) are missing. This usually means a destroy-mode entry point called into create/update by mistake.`);
31907
+ }
31908
+ deriveChildStackName(parentStackName, nestedLogicalId) {
31909
+ return `${parentStackName}~${nestedLogicalId}`;
31910
+ }
31911
+ synthesizeArn(accountId, region, parentStackName, logicalId) {
31912
+ return `arn:cdkd-local:${region}:${accountId}:nested-stack/${parentStackName}/${logicalId}`;
31913
+ }
31914
+ extractParameters(properties) {
31915
+ const params = properties["Parameters"];
31916
+ if (!params || typeof params !== "object" || Array.isArray(params)) return {};
31917
+ const result = {};
31918
+ for (const [k, v] of Object.entries(params)) if (typeof v === "string") result[k] = v;
31919
+ else if (typeof v === "number" || typeof v === "boolean") result[k] = String(v);
31920
+ else throw new Error(`NestedStackProvider: child Parameter '${k}' resolved to a non-scalar value (type=${v === null ? "null" : typeof v}). Parameters must be scalars (string / number / boolean) by the time they reach the provider — an unresolved intrinsic here means IntrinsicFunctionResolver upstream did not handle the value, which is a bug. Surface the unresolved input rather than silently coercing to '[object Object]'.`);
31921
+ return result;
31922
+ }
31923
+ readChildTemplate(templatePath) {
31924
+ let raw;
31925
+ try {
31926
+ raw = fs.readFileSync(templatePath, "utf-8");
31927
+ } catch (err) {
31928
+ throw new Error(`Failed to read nested template at ${templatePath}: ${err instanceof Error ? err.message : String(err)}`);
31929
+ }
31930
+ try {
31931
+ return JSON.parse(raw);
31932
+ } catch (err) {
31933
+ throw new Error(`Failed to parse nested template at ${templatePath}: ${err instanceof Error ? err.message : String(err)}`);
31934
+ }
31935
+ }
31936
+ indexGrandchildTemplates(childTemplate, childTemplatePath) {
31937
+ const dir = path.dirname(childTemplatePath);
31938
+ const result = {};
31939
+ for (const [grandLogicalId, resource] of Object.entries(childTemplate.Resources ?? {})) {
31940
+ if (resource?.Type !== "AWS::CloudFormation::Stack") continue;
31941
+ const assetPath = resource.Metadata?.["aws:asset:path"];
31942
+ if (typeof assetPath !== "string" || assetPath.length === 0) continue;
31943
+ if (path.isAbsolute(assetPath)) throw new Error(`NestedStackProvider: nested-stack '${grandLogicalId}' has Metadata['aws:asset:path']='${assetPath}' which is absolute. CDK emits relative asset paths for nested templates; an absolute path indicates the synth output was hand-modified or generated by a non-CDK toolchain. Refusing to load.`);
31944
+ result[grandLogicalId] = path.join(dir, assetPath);
31945
+ }
31946
+ return result;
31947
+ }
31948
+ buildOutputsAttributes(outputs) {
31949
+ const attributes = {};
31950
+ for (const [key, value] of Object.entries(outputs)) attributes[`Outputs.${key}`] = value;
31951
+ return attributes;
31952
+ }
31953
+ };
31954
+
30947
31955
  //#endregion
30948
31956
  //#region src/provisioning/register-providers.ts
30949
31957
  /**
@@ -31079,6 +32087,7 @@ function registerAllProviders(registry) {
31079
32087
  registry.register("AWS::S3Tables::TableBucket", s3TablesProvider);
31080
32088
  registry.register("AWS::S3Tables::Namespace", s3TablesProvider);
31081
32089
  registry.register("AWS::S3Tables::Table", s3TablesProvider);
32090
+ registry.register("AWS::CloudFormation::Stack", new NestedStackProvider());
31082
32091
  }
31083
32092
 
31084
32093
  //#endregion
@@ -31356,7 +32365,7 @@ async function deployCommand(stacks, options) {
31356
32365
  if (!await promptMigrationConfirm(pending, { yes: options.yes })) return;
31357
32366
  }
31358
32367
  }
31359
- const deployResult = await new DeployEngine(stackStateBackend, stackLockManager, dagBuilder, diffCalculator, stackProviderRegistry, {
32368
+ const deployEngineOptions = {
31360
32369
  concurrency: options.concurrency,
31361
32370
  dryRun: options.dryRun,
31362
32371
  noRollback: !options.rollback,
@@ -31365,7 +32374,23 @@ async function deployCommand(stacks, options) {
31365
32374
  ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
31366
32375
  ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
31367
32376
  ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
31368
- }, stackRegion, exportIndexStore).deploy(stackInfo.stackName, stackInfo.template);
32377
+ };
32378
+ const stackDeployEngine = new DeployEngine(stackStateBackend, stackLockManager, dagBuilder, diffCalculator, stackProviderRegistry, deployEngineOptions, stackRegion, exportIndexStore);
32379
+ const deployResult = await withNestedStackContext({
32380
+ stateBackend: stackStateBackend,
32381
+ lockManager: stackLockManager,
32382
+ providerRegistry: stackProviderRegistry,
32383
+ parentStackName: stackInfo.stackName,
32384
+ parentRegion: stackRegion,
32385
+ accountId,
32386
+ awsClients: stackAwsClients,
32387
+ stateBucket,
32388
+ exportIndexStore,
32389
+ nestedTemplates: stackInfo.nestedTemplates ?? {},
32390
+ dagBuilder,
32391
+ diffCalculator,
32392
+ options: deployEngineOptions
32393
+ }, () => stackDeployEngine.deploy(stackInfo.stackName, stackInfo.template));
31369
32394
  logger.info(`\n${bold("Deployment Summary:")}`);
31370
32395
  logger.info(` Stack: ${bold(cyan(deployResult.stackName))}`);
31371
32396
  logger.info(` Created: ${deployResult.created > 0 ? green(deployResult.created) : gray(deployResult.created)}`);
@@ -32475,342 +33500,6 @@ function createDriftCommand() {
32475
33500
  return cmd;
32476
33501
  }
32477
33502
 
32478
- //#endregion
32479
- //#region src/cli/commands/destroy-runner.ts
32480
- /**
32481
- * Resource-type → state-property name pairs that gate AWS deletion
32482
- * protection. Used by the `--remove-protection` confirmation prompt to
32483
- * report a best-effort count of resources that will have protection
32484
- * cleared. The actual flip-off is unconditional inside each provider's
32485
- * `delete()` (idempotent — safe when AWS already has protection off),
32486
- * so the count is informational only.
32487
- *
32488
- * Most types use a boolean flag — the value `true` is what we count.
32489
- * Two types use a string-valued enum (Cognito UserPool's
32490
- * `DeletionProtection` is `'ACTIVE' | 'INACTIVE'`, AutoScalingGroup's
32491
- * `DeletionProtection` is `'none' | 'prevent-force-deletion' |
32492
- * 'prevent-all-deletion'`). For those, the helper checks against a
32493
- * per-type set of "active" values via `PROTECTION_ACTIVE_VALUES_BY_TYPE`.
32494
- *
32495
- * Exported for unit-test coverage of `countProtectedResources`.
32496
- */
32497
- const PROTECTION_PROPERTY_BY_TYPE = {
32498
- "AWS::Logs::LogGroup": "DeletionProtectionEnabled",
32499
- "AWS::RDS::DBInstance": "DeletionProtection",
32500
- "AWS::RDS::DBCluster": "DeletionProtection",
32501
- "AWS::DocDB::DBCluster": "DeletionProtection",
32502
- "AWS::Neptune::DBCluster": "DeletionProtection",
32503
- "AWS::Neptune::DBInstance": "DeletionProtection",
32504
- "AWS::DynamoDB::Table": "DeletionProtectionEnabled",
32505
- "AWS::DynamoDB::GlobalTable": "DeletionProtectionEnabled",
32506
- "AWS::EC2::Instance": "DisableApiTermination",
32507
- "AWS::Cognito::UserPool": "DeletionProtection",
32508
- "AWS::AutoScaling::AutoScalingGroup": "DeletionProtection"
32509
- };
32510
- /**
32511
- * For string-valued protection enums, the set of values that count as
32512
- * "currently protected". Types absent from this map use the default
32513
- * (boolean `true`).
32514
- */
32515
- const PROTECTION_ACTIVE_VALUES_BY_TYPE = {
32516
- "AWS::Cognito::UserPool": new Set(["ACTIVE"]),
32517
- "AWS::AutoScaling::AutoScalingGroup": new Set(["prevent-force-deletion", "prevent-all-deletion"])
32518
- };
32519
- /**
32520
- * Count how many resources in a stack's recorded state appear to have
32521
- * deletion protection enabled. Walks `properties` and `observedProperties`
32522
- * for the property name registered against each resource type in
32523
- * `PROTECTION_PROPERTY_BY_TYPE`. ELBv2 LoadBalancer protection lives in
32524
- * `LoadBalancerAttributes` (a CFn `Array<{Key, Value}>`), so it's
32525
- * handled separately via the `deletion_protection.enabled` key.
32526
- */
32527
- function countProtectedResources(state) {
32528
- let count = 0;
32529
- for (const resource of Object.values(state.resources ?? {})) {
32530
- const propName = PROTECTION_PROPERTY_BY_TYPE[resource.resourceType];
32531
- if (propName) {
32532
- const recorded = resource.properties?.[propName] ?? resource.observedProperties?.[propName];
32533
- const activeValues = PROTECTION_ACTIVE_VALUES_BY_TYPE[resource.resourceType];
32534
- if (activeValues) {
32535
- if (activeValues.has(recorded)) count++;
32536
- } else if (recorded === true) count++;
32537
- continue;
32538
- }
32539
- if (resource.resourceType === "AWS::ElasticLoadBalancingV2::LoadBalancer") {
32540
- if (((resource.properties?.["LoadBalancerAttributes"] ?? resource.observedProperties?.["LoadBalancerAttributes"])?.find((a) => a?.Key === "deletion_protection.enabled"))?.Value === "true") count++;
32541
- }
32542
- }
32543
- return count;
32544
- }
32545
- /**
32546
- * Run the destroy lifecycle for one stack against an already-loaded
32547
- * `StackState`, reusing the caller's state backend / lock manager.
32548
- *
32549
- * Hoisted from `cdkd destroy` so the new `cdkd state destroy` subcommand
32550
- * can call into the exact same per-stack pipeline without depending on
32551
- * synth or the CDK app. The state-source split is the only meaningful
32552
- * difference between the two commands — everything from "prompt the user"
32553
- * onwards is identical.
32554
- *
32555
- * Side effects:
32556
- * - Acquires (and releases) the stack's S3 lock.
32557
- * - Switches `process.env.AWS_REGION` for the duration of the destroy when
32558
- * the stack's recorded region differs from `baseRegion`. Restored in the
32559
- * `finally` block.
32560
- * - On full success, deletes the state file. On any failure, the state
32561
- * file is preserved so the user can retry.
32562
- */
32563
- async function runDestroyForStack(stackName, state, ctx) {
32564
- const logger = getLogger();
32565
- const result = {
32566
- stackName,
32567
- cancelled: false,
32568
- skippedEmpty: false,
32569
- deletedCount: 0,
32570
- retainedCount: 0,
32571
- errorCount: 0
32572
- };
32573
- const resourceCount = Object.keys(state.resources).length;
32574
- const regionForState = state.region ?? ctx.baseRegion;
32575
- if (resourceCount === 0) {
32576
- logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
32577
- await ctx.stateBackend.deleteState(stackName, regionForState);
32578
- logger.info(`${green("✓")} State deleted`);
32579
- result.skippedEmpty = true;
32580
- return result;
32581
- }
32582
- const needsStrongRefCheck = !!(state.outputs && Object.keys(state.outputs).length > 0);
32583
- if (needsStrongRefCheck) {
32584
- const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
32585
- if (consumers.length > 0) throw new StackHasActiveImportsError(stackName, regionForState, consumers);
32586
- }
32587
- logger.info(`\nResources to be deleted (${resourceCount}):`);
32588
- for (const [logicalId, resource] of Object.entries(state.resources)) logger.info(` - ${logicalId} (${resource.resourceType})`);
32589
- const protectedCount = ctx.removeProtection ? countProtectedResources(state) : 0;
32590
- if (!ctx.skipConfirmation) {
32591
- const rl = readline.createInterface({
32592
- input: process.stdin,
32593
- output: process.stdout
32594
- });
32595
- const prompt = ctx.removeProtection ? `\nAbout to destroy ${resourceCount} resources from stack "${stackName}", REMOVING DELETION PROTECTION on ${protectedCount} of them. Continue? (y/N): ` : `\nAre you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `;
32596
- const answer = await rl.question(prompt);
32597
- rl.close();
32598
- const trimmed = answer.trim().toLowerCase();
32599
- if (ctx.removeProtection) {
32600
- if (trimmed !== "y" && trimmed !== "yes") {
32601
- logger.info("Destroy cancelled");
32602
- result.cancelled = true;
32603
- return result;
32604
- }
32605
- } else if (trimmed === "n" || trimmed === "no") {
32606
- logger.info("Destroy cancelled");
32607
- result.cancelled = true;
32608
- return result;
32609
- }
32610
- }
32611
- const stackRegion = state.region;
32612
- let destroyProviderRegistry = ctx.providerRegistry;
32613
- let destroyAwsClients;
32614
- if (stackRegion && stackRegion !== ctx.baseRegion) {
32615
- logger.info(`Stack region: ${stackRegion}`);
32616
- process.env["AWS_REGION"] = stackRegion;
32617
- process.env["AWS_DEFAULT_REGION"] = stackRegion;
32618
- destroyAwsClients = new AwsClients({
32619
- region: stackRegion,
32620
- ...ctx.profile && { profile: ctx.profile }
32621
- });
32622
- setAwsClients(destroyAwsClients);
32623
- destroyProviderRegistry = new ProviderRegistry();
32624
- registerAllProviders(destroyProviderRegistry);
32625
- destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
32626
- }
32627
- logger.info(`\nAcquiring lock for stack ${stackName}...`);
32628
- await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
32629
- if (needsStrongRefCheck) {
32630
- const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
32631
- if (consumers.length > 0) {
32632
- try {
32633
- await ctx.lockManager.releaseLock(stackName, regionForState);
32634
- } catch (releaseErr) {
32635
- logger.warn(`Failed to release lock after strong-ref refusal: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
32636
- }
32637
- throw new StackHasActiveImportsError(stackName, regionForState, consumers);
32638
- }
32639
- }
32640
- const renderer = getLiveRenderer();
32641
- renderer.start();
32642
- try {
32643
- logger.info("Building dependency graph...");
32644
- const template = {
32645
- AWSTemplateFormatVersion: "2010-09-09",
32646
- Resources: {}
32647
- };
32648
- for (const [logicalId, resource] of Object.entries(state.resources)) template.Resources[logicalId] = {
32649
- Type: resource.resourceType,
32650
- Properties: resource.properties || {},
32651
- ...resource.dependencies && resource.dependencies.length > 0 && { DependsOn: resource.dependencies }
32652
- };
32653
- const typeToLogicalIds = /* @__PURE__ */ new Map();
32654
- for (const [logicalId, resource] of Object.entries(state.resources)) {
32655
- const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
32656
- ids.push(logicalId);
32657
- typeToLogicalIds.set(resource.resourceType, ids);
32658
- }
32659
- for (const [logicalId, resource] of Object.entries(state.resources)) {
32660
- const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
32661
- if (!mustDeleteAfter) continue;
32662
- for (const depType of mustDeleteAfter) {
32663
- const depIds = typeToLogicalIds.get(depType);
32664
- if (!depIds) continue;
32665
- for (const depId of depIds) {
32666
- const existing = template.Resources[depId]?.DependsOn ?? [];
32667
- const depsArray = Array.isArray(existing) ? existing : [existing];
32668
- if (!depsArray.includes(logicalId)) {
32669
- template.Resources[depId] = {
32670
- ...template.Resources[depId],
32671
- DependsOn: [...depsArray, logicalId]
32672
- };
32673
- logger.debug(`Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`);
32674
- }
32675
- }
32676
- }
32677
- }
32678
- const dagBuilder = new DagBuilder();
32679
- const graph = dagBuilder.buildGraph(template);
32680
- const executionLevels = dagBuilder.getExecutionLevels(graph);
32681
- logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
32682
- for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
32683
- const level = executionLevels[levelIndex];
32684
- if (!level) continue;
32685
- logger.debug(`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`);
32686
- const stackRegion = state.region ?? ctx.baseRegion;
32687
- const deletePromises = level.map(async (logicalId) => {
32688
- const resource = state.resources[logicalId];
32689
- if (!resource) {
32690
- logger.warn(`Resource ${logicalId} not found in state, skipping`);
32691
- return;
32692
- }
32693
- if (shouldRetainResource(resource.deletionPolicy)) {
32694
- logger.info(` ⊘ ${logicalId} (${resource.resourceType}) retained — DeletionPolicy: ${resource.deletionPolicy}`);
32695
- result.retainedCount++;
32696
- return;
32697
- }
32698
- const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
32699
- renderer.addTask(logicalId, baseLabel);
32700
- try {
32701
- const provider = destroyProviderRegistry.getProvider(resource.resourceType);
32702
- const providerMinTimeoutMs = provider.getMinResourceTimeoutMs?.() ?? 0;
32703
- const warnAfterMs = ctx.resourceWarnAfterByType?.[resource.resourceType] ?? ctx.resourceWarnAfterMs ?? 3e5;
32704
- const globalTimeoutMs = ctx.resourceTimeoutMs ?? 18e5;
32705
- const timeoutMs = ctx.resourceTimeoutByType?.[resource.resourceType] ?? Math.max(providerMinTimeoutMs, globalTimeoutMs);
32706
- await withResourceDeadline(async () => {
32707
- const maxAttempts = provider.disableOuterRetry ? 0 : 3;
32708
- let lastDeleteError;
32709
- for (let attempt = 0; attempt <= maxAttempts; attempt++) try {
32710
- await provider.delete(logicalId, resource.physicalId, resource.resourceType, resource.properties, {
32711
- ...state.region !== void 0 && { expectedRegion: state.region },
32712
- ...ctx.removeProtection === true && { removeProtection: true }
32713
- });
32714
- lastDeleteError = null;
32715
- break;
32716
- } catch (retryError) {
32717
- lastDeleteError = retryError;
32718
- const msg = retryError instanceof Error ? retryError.message : String(retryError);
32719
- if (!(msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation")) || attempt >= maxAttempts) break;
32720
- const delay = 5e3 * Math.pow(2, attempt);
32721
- logger.debug(` ⏳ Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxAttempts})`);
32722
- await new Promise((resolve) => setTimeout(resolve, delay));
32723
- }
32724
- if (lastDeleteError) throw lastDeleteError;
32725
- }, {
32726
- warnAfterMs,
32727
- timeoutMs,
32728
- onWarn: (elapsedMs) => {
32729
- const minutes = Math.max(1, Math.round(elapsedMs / 6e4));
32730
- renderer.updateTaskLabel(logicalId, `${baseLabel} [taking longer than expected, ${minutes}m+]`);
32731
- renderer.printAbove(() => {
32732
- logger.warn(`${logicalId} (${resource.resourceType}) has been deleting for ${minutes}m — still waiting`);
32733
- });
32734
- },
32735
- onTimeout: (elapsedMs) => new ResourceTimeoutError(logicalId, resource.resourceType, stackRegion, elapsedMs, "DELETE", timeoutMs)
32736
- });
32737
- renderer.removeTask(logicalId);
32738
- logger.info(` ${red("✗")} ${bold(logicalId)} ${gray(`(${resource.resourceType})`)} ${red("deleted")}`);
32739
- result.deletedCount++;
32740
- } catch (error) {
32741
- renderer.removeTask(logicalId);
32742
- const msg = error instanceof Error ? error.message : String(error);
32743
- if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
32744
- logger.debug(` ${logicalId} already deleted, removing from state`);
32745
- result.deletedCount++;
32746
- } else if (error instanceof ResourceTimeoutError) {
32747
- const wrapped = new ProvisioningError(error.message, resource.resourceType, logicalId, resource.physicalId, error);
32748
- logger.error(` ✗ Failed to delete ${logicalId}:`, wrapped.message);
32749
- result.errorCount++;
32750
- } else {
32751
- logger.error(` ✗ Failed to delete ${logicalId}:`, String(error));
32752
- result.errorCount++;
32753
- }
32754
- } finally {
32755
- renderer.removeTask(logicalId);
32756
- }
32757
- });
32758
- await Promise.all(deletePromises);
32759
- }
32760
- if (result.errorCount === 0) {
32761
- await ctx.stateBackend.deleteState(stackName, regionForState);
32762
- logger.debug("State deleted");
32763
- if (ctx.exportIndexStore) await ctx.exportIndexStore.removeStack(stackName, regionForState);
32764
- } else logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
32765
- const retainedSuffix = result.retainedCount > 0 ? `, ${result.retainedCount} retained` : "";
32766
- if (result.errorCount === 0) logger.info(`\n${green("✓")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
32767
- else logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} partially destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${red(result.errorCount)} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to clean up.`);
32768
- } finally {
32769
- renderer.stop();
32770
- logger.debug("Releasing lock...");
32771
- await ctx.lockManager.releaseLock(stackName, regionForState);
32772
- if (destroyAwsClients) {
32773
- destroyAwsClients.destroy();
32774
- process.env["AWS_REGION"] = ctx.baseRegion;
32775
- process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
32776
- setAwsClients(ctx.baseAwsClients);
32777
- }
32778
- }
32779
- return result;
32780
- }
32781
- /**
32782
- * Strong-reference scan: read every other stack's state.json from the
32783
- * state bucket and check whether any of its `imports[]` entries names
32784
- * `producerStack`. Returns the list of offending consumers (possibly
32785
- * empty).
32786
- *
32787
- * NEVER trusts the persistent exports index — a stale index could miss
32788
- * a freshly-recorded consumer and let a destructive destroy through.
32789
- * The cost is one `listStacks` + N parallel GETs at destroy time only
32790
- * (not the deploy hot path), which the user-facing UX rationalizes as
32791
- * the "destroy is slow OK" trade-off (Issue #343).
32792
- */
32793
- async function scanActiveConsumers(producerStack, producerRegion, ctx) {
32794
- const refs = await ctx.stateBackend.listStacks();
32795
- return (await Promise.all(refs.map(async (ref) => {
32796
- const region = ref.region ?? ctx.baseRegion;
32797
- if (ref.stackName === producerStack && region === producerRegion) return null;
32798
- try {
32799
- const imports = (await ctx.stateBackend.getState(ref.stackName, region))?.state.imports;
32800
- if (!imports || imports.length === 0) return null;
32801
- const matches = imports.filter((entry) => entry.sourceStack === producerStack && entry.sourceRegion === producerRegion);
32802
- if (matches.length === 0) return null;
32803
- return matches.map((entry) => ({
32804
- consumerStack: ref.stackName,
32805
- consumerRegion: region,
32806
- exportName: entry.exportName
32807
- }));
32808
- } catch {
32809
- return null;
32810
- }
32811
- }))).filter((r) => r !== null).flat();
32812
- }
32813
-
32814
33503
  //#endregion
32815
33504
  //#region src/cli/commands/destroy.ts
32816
33505
  /**
@@ -32904,6 +33593,7 @@ async function destroyCommand(stackArgs, options) {
32904
33593
  return;
32905
33594
  }
32906
33595
  logger.info(`Found ${stackNames.length} stack(s) to destroy: ${stackNames.join(", ")}`);
33596
+ const accountId = "unknown";
32907
33597
  const stateRefsByName = /* @__PURE__ */ new Map();
32908
33598
  for (const ref of allStateRefs) {
32909
33599
  const arr = stateRefsByName.get(ref.stackName) ?? [];
@@ -32941,7 +33631,25 @@ async function destroyCommand(stackArgs, options) {
32941
33631
  logger.warn(`No state found for stack ${stackName}, skipping`);
32942
33632
  continue;
32943
33633
  }
32944
- const result = await runDestroyForStack(stackName, stateResult.state, {
33634
+ const result = await withNestedStackContext({
33635
+ stateBackend,
33636
+ lockManager,
33637
+ providerRegistry,
33638
+ parentStackName: stackName,
33639
+ parentRegion: stackTargetRegion,
33640
+ accountId,
33641
+ awsClients,
33642
+ stateBucket,
33643
+ exportIndexStore,
33644
+ destroyOptions: {
33645
+ ...options.profile && { profile: options.profile },
33646
+ ...options.removeProtection === true && { removeProtection: true },
33647
+ ...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
33648
+ ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
33649
+ ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
33650
+ ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
33651
+ }
33652
+ }, () => runDestroyForStack(stackName, stateResult.state, {
32945
33653
  stateBackend,
32946
33654
  lockManager,
32947
33655
  providerRegistry,
@@ -32956,7 +33664,7 @@ async function destroyCommand(stackArgs, options) {
32956
33664
  ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
32957
33665
  ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
32958
33666
  ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
32959
- });
33667
+ }));
32960
33668
  totalErrors += result.errorCount;
32961
33669
  }
32962
33670
  if (totalErrors > 0) throw new PartialFailureError(`Destroy completed with ${totalErrors} resource error(s). State preserved — inspect 'cdkd state show <stack>' and re-run 'cdkd destroy' to retry.`);
@@ -34690,7 +35398,25 @@ async function stateDestroyCommand(stackArgs, options) {
34690
35398
  logger.warn(`No state found for stack ${stackName}${ref.region ? ` in ${ref.region}` : ""}, skipping`);
34691
35399
  continue;
34692
35400
  }
34693
- const result = await runDestroyForStack(stackName, stateResult.state, {
35401
+ const result = await withNestedStackContext({
35402
+ stateBackend: setup.stateBackend,
35403
+ lockManager: setup.lockManager,
35404
+ providerRegistry,
35405
+ parentStackName: stackName,
35406
+ parentRegion: ref.region ?? setup.region,
35407
+ accountId: "unknown",
35408
+ awsClients: setup.awsClients,
35409
+ stateBucket: setup.bucket,
35410
+ exportIndexStore: setup.exportIndexStore,
35411
+ destroyOptions: {
35412
+ ...options.profile && { profile: options.profile },
35413
+ ...options.removeProtection === true && { removeProtection: true },
35414
+ ...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
35415
+ ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
35416
+ ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
35417
+ ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
35418
+ }
35419
+ }, () => runDestroyForStack(stackName, stateResult.state, {
34694
35420
  stateBackend: setup.stateBackend,
34695
35421
  lockManager: setup.lockManager,
34696
35422
  providerRegistry,
@@ -34705,7 +35431,7 @@ async function stateDestroyCommand(stackArgs, options) {
34705
35431
  ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
34706
35432
  ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
34707
35433
  ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
34708
- });
35434
+ }));
34709
35435
  totalErrors += result.errorCount;
34710
35436
  }
34711
35437
  }
@@ -52812,8 +53538,11 @@ function createLocalCommand() {
52812
53538
  *
52813
53539
  * - `AWS::CDK::Metadata` is a CDK sentinel; not a real AWS resource and
52814
53540
  * CFn refuses to import it.
52815
- * - `AWS::CloudFormation::Stack` is a nested stack reference; importing
52816
- * means re-creating the child stack, not adopting AWS resources.
53541
+ * - `AWS::CloudFormation::Stack` is a nested stack reference. Fresh
53542
+ * `cdkd deploy` of nested stacks IS supported (issue #459), but
53543
+ * moving an existing nested-stack hierarchy from cdkd back into
53544
+ * CloudFormation via `cdkd export` is deferred to the
53545
+ * [#464](https://github.com/go-to-k/cdkd/issues/464) follow-up.
52817
53546
  * - `AWS::CloudFormation::CustomResource` is the CFn resource type CDK
52818
53547
  * emits for `new cdk.CustomResource(...)` when no `resourceType` is
52819
53548
  * passed. Functionally identical to `Custom::*` — Lambda-backed,
@@ -53270,7 +53999,10 @@ async function assertCfnStackAbsent(cfnClient, stackName) {
53270
53999
  * `AWS::CloudFormation::Stack` (nested stacks) is intentionally NOT in
53271
54000
  * this set: CFn would CREATE a duplicate nested stack rather than adopt
53272
54001
  * the existing one, which would conflict with whatever the cdkd state
53273
- * thought it owned. cdkd doesn't deploy nested stacks anyway.
54002
+ * thought it owned. Fresh `cdkd deploy` of nested stacks is supported
54003
+ * via the recursive `NestedStackProvider` (#459), but `cdkd export`
54004
+ * adoption back into CloudFormation is deferred to the
54005
+ * [#464](https://github.com/go-to-k/cdkd/issues/464) follow-up.
53274
54006
  *
53275
54007
  * Exported for unit testing.
53276
54008
  */
@@ -54222,10 +54954,14 @@ function compareSemver(a, b) {
54222
54954
  * different from the metadata-transfer migration this command
54223
54955
  * provides.
54224
54956
  *
54225
- * - `AWS::CloudFormation::Stack` — nested stacks. cdkd has no
54226
- * provider for this type, and the matching `cdk migrate` output
54227
- * flattens nested stacks into separate generated apps in a way
54228
- * that doesn't round-trip cleanly. Out of scope for #465.
54957
+ * - `AWS::CloudFormation::Stack` — nested stacks. cdkd's recursive
54958
+ * `NestedStackProvider` handles fresh `cdkd deploy` (#459), but
54959
+ * `cdk migrate` flattens nested stacks into separate generated apps
54960
+ * in a way that doesn't round-trip cleanly into cdkd's parent~child
54961
+ * state-key layout, so the migrate command keeps rejecting them.
54962
+ * Adopting an existing CFn-managed nested-stack hierarchy is
54963
+ * deferred to issue
54964
+ * [#464](https://github.com/go-to-k/cdkd/issues/464).
54229
54965
  *
54230
54966
  * - `Custom::*` — any user-defined Custom Resource type prefix.
54231
54967
  * Same rationale as `AWS::CloudFormation::CustomResource`.
@@ -55239,7 +55975,7 @@ function reorderArgs(argv) {
55239
55975
  */
55240
55976
  async function main() {
55241
55977
  const program = new Command();
55242
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.143.0");
55978
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.145.0");
55243
55979
  program.addCommand(createBootstrapCommand());
55244
55980
  program.addCommand(createSynthCommand());
55245
55981
  program.addCommand(createListCommand());