@go-to-k/cdkd 0.283.4 → 0.283.6

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,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as configStringRefusal, $t as resolveSkipPrefix, A as green, At as rewriteTemplateAssetReferences, B as slowCcOperationTimeoutMs, Bt as formatDockerLoginError, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as CdkdError, Ct as shouldRetainResource, D as bold, Dn as LocalInvokeBuildError$1, Dt as buildAssetRedirectMap, E as formatResourceLine, En as DeployCancelledError, Et as WorkGraph, F as clearOnUpdateRemoval, Fn as ResourceTimeoutError, Ft as parseBootstrapMarker, G as getAccountInfo, Gt as getDockerImageBySourceHash, H as isTerminationProtectionPropagationError, Ht as runDockerForeground, I as ProviderRegistry, In as ResourceUpdateNotSupportedError, It as validateAssetBucketName, J as normalizeAwsTagsToCfn, Jt as getDefaultStateBucketName, K as refStateLookupFromResource, Kt as Synthesizer, L as findActionableSilentDrops, Ln as StackHasActiveImportsError, Lt as validateContainerRepoName, M as yellow, Mn as NestedStackChildDirectDestroyError, Mt as BOOTSTRAP_MARKER_PREFIX, N as IAMRoleProvider, Nn as PartialFailureError, Nt as ensureAssetStorage, O as cyan, On as LocalMigrateError, Ot as createAssetRedirectResolver, P as collectInlinePolicyNamesManagedBySiblings, Pn as ProvisioningError, Pt as getBootstrapMarkerKey, Q as configBooleanRefusal, Qt as resolveCaptureObservedState, R as findSilentDropProperties, Rn as StackTerminationProtectionError, Rt as buildDenyExternalAccessPolicy, S as extractDeploymentEventError, St as rebuildClientForBucketRegion, T as renderStatefulReason, Tt as stringifyValue, U as IntrinsicFunctionResolver, Un as normalizeAwsError, Ut as runDockerStreaming, V as disableInstanceApiTermination, Vt as getDockerCmd, W as cfnRefValueFromPhysicalId, Wn as withErrorHandling, Wt as AssetManifestLoader, X as assertRegionMatch, Xt as resolveApp, Y as resolveExplicitPhysicalId, Yt as getLegacyStateBucketName, Z as coerceCfnBoolean, Zt as resolveAutoAssetStorage, _ as createPreDeleteFinalSnapshot, _n as resolveBucketRegion, _t as markNonRetryable, a as DeploymentEventsStore, an as CFN_TEMPLATE_BODY_LIMIT, at as s3BucketArn, b as unsupportedFinalSnapshotError, bn as resetAwsClients, bt as LockManager, c as replayFailedOperations, cn as findLargeInlineResources, ct as s3BucketRegionalDomainName, d as IMPLICIT_DELETE_DEPENDENCIES, dn as PARTITION_TABLE, dt as DiffCalculator, en as resolveStateBucketWithDefault, et as readConfigString, f as computeImplicitDeleteEdges, fn as canonicalizeRegion, ft as describeTypeWithThrottleRetry, g as ccRoutedFinalSnapshotError, gt as isThrottlingError, h as buildFinalSnapshotIdentifier, hn as processStackMessages, ht as isRetryableTransientError, i as DeploymentEventsReader, in as warnDeprecatedNoPrefixCliFlag, it as requireConfigString, j as red, jn as MissingCdkCliError, jt as AssetModeResolver, k as gray, kn as LocalStartServiceError, kt as loadPublishableAssetManifest, l as replayRollback, ln as uploadCfnTemplate, lt as s3BucketWebsiteUrl, m as PRE_DELETE_SNAPSHOT_TYPES, mn as AssemblyReader, mt as isMarkedNonRetryable, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as resolveUseCdkBootstrapAssets, nt as requireConfigArray, o as planFailedOps, on as CFN_TEMPLATE_URL_LIMIT, ot as s3BucketDomainName, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as derivePartitionAndUrlSuffix, pt as withRetry, q as WAFv2WebACLProvider, qt as synthesisStatusMessage, r as DeployEngine, rn as stateBucketExistenceConfirmed, rt as requireConfigObject, s as planRollback, sn as MIGRATE_TMP_PREFIX, st as s3BucketDualStackDomainName, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveStateBucketWithDefaultAndSource, tt as replayWarn, u as withResourceDeadline, un as expectedOwnerParam, ut as applyRoleArnIfSet, v as isFinalSnapshotError, vn as AwsClients, vt as DagBuilder, w as isStatefulRecreateTargetSync, wt as AssetPublisher, x as makeCanonicalizePropertiesFn, xn as setAwsClients, xt as S3StateBackend, y as refusesFinalSnapshot, yn as getAwsClients, yt as TemplateParser, z as CloudControlProvider, zt as buildDockerImage } from "./deploy-engine-C605sBVJ.js";
2
+ import { $ as configStringRefusal, $t as resolveSkipPrefix, A as green, At as rewriteTemplateAssetReferences, B as slowCcOperationTimeoutMs, Bt as formatDockerLoginError, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as CdkdError, Ct as shouldRetainResource, D as bold, Dn as LocalInvokeBuildError$1, Dt as buildAssetRedirectMap, E as formatResourceLine, En as DeployCancelledError, Et as WorkGraph, F as clearOnUpdateRemoval, Fn as ResourceTimeoutError, Ft as parseBootstrapMarker, G as getAccountInfo, Gt as getDockerImageBySourceHash, H as isTerminationProtectionPropagationError, Ht as runDockerForeground, I as ProviderRegistry, In as ResourceUpdateNotSupportedError, It as validateAssetBucketName, J as normalizeAwsTagsToCfn, Jt as getDefaultStateBucketName, K as refStateLookupFromResource, Kt as Synthesizer, L as findActionableSilentDrops, Ln as StackHasActiveImportsError, Lt as validateContainerRepoName, M as yellow, Mn as NestedStackChildDirectDestroyError, Mt as BOOTSTRAP_MARKER_PREFIX, N as IAMRoleProvider, Nn as PartialFailureError, Nt as ensureAssetStorage, O as cyan, On as LocalMigrateError, Ot as createAssetRedirectResolver, P as collectInlinePolicyNamesManagedBySiblings, Pn as ProvisioningError, Pt as getBootstrapMarkerKey, Q as configBooleanRefusal, Qt as resolveCaptureObservedState, R as findSilentDropProperties, Rn as StackTerminationProtectionError, Rt as buildDenyExternalAccessPolicy, S as extractDeploymentEventError, St as rebuildClientForBucketRegion, T as renderStatefulReason, Tt as stringifyValue, U as IntrinsicFunctionResolver, Un as normalizeAwsError, Ut as runDockerStreaming, V as disableInstanceApiTermination, Vt as getDockerCmd, W as cfnRefValueFromPhysicalId, Wn as withErrorHandling, Wt as AssetManifestLoader, X as assertRegionMatch, Xt as resolveApp, Y as resolveExplicitPhysicalId, Yt as getLegacyStateBucketName, Z as coerceCfnBoolean, Zt as resolveAutoAssetStorage, _ as createPreDeleteFinalSnapshot, _n as resolveBucketRegion, _t as markNonRetryable, a as DeploymentEventsStore, an as CFN_TEMPLATE_BODY_LIMIT, at as s3BucketArn, b as unsupportedFinalSnapshotError, bn as resetAwsClients, bt as LockManager, c as replayFailedOperations, cn as findLargeInlineResources, ct as s3BucketRegionalDomainName, d as IMPLICIT_DELETE_DEPENDENCIES, dn as PARTITION_TABLE, dt as DiffCalculator, en as resolveStateBucketWithDefault, et as readConfigString, f as computeImplicitDeleteEdges, fn as canonicalizeRegion, ft as describeTypeWithThrottleRetry, g as ccRoutedFinalSnapshotError, gt as isThrottlingError, h as buildFinalSnapshotIdentifier, hn as processStackMessages, ht as isRetryableTransientError, i as DeploymentEventsReader, in as warnDeprecatedNoPrefixCliFlag, it as requireConfigString, j as red, jn as MissingCdkCliError, jt as AssetModeResolver, k as gray, kn as LocalStartServiceError, kt as loadPublishableAssetManifest, l as replayRollback, ln as uploadCfnTemplate, lt as s3BucketWebsiteUrl, m as PRE_DELETE_SNAPSHOT_TYPES, mn as AssemblyReader, mt as isMarkedNonRetryable, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as resolveUseCdkBootstrapAssets, nt as requireConfigArray, o as planFailedOps, on as CFN_TEMPLATE_URL_LIMIT, ot as s3BucketDomainName, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as derivePartitionAndUrlSuffix, pt as withRetry, q as WAFv2WebACLProvider, qt as synthesisStatusMessage, r as DeployEngine, rn as stateBucketExistenceConfirmed, rt as requireConfigObject, s as planRollback, sn as MIGRATE_TMP_PREFIX, st as s3BucketDualStackDomainName, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveStateBucketWithDefaultAndSource, tt as replayWarn, u as withResourceDeadline, un as expectedOwnerParam, ut as applyRoleArnIfSet, v as isFinalSnapshotError, vn as AwsClients, vt as DagBuilder, w as isStatefulRecreateTargetSync, wt as AssetPublisher, x as makeCanonicalizePropertiesFn, xn as setAwsClients, xt as S3StateBackend, y as refusesFinalSnapshot, yn as getAwsClients, yt as TemplateParser, z as CloudControlProvider, zt as buildDockerImage } from "./deploy-engine-DaEsg4mn.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
4
- import { t as ASGProvider } from "./asg-provider-CZQewQcH.js";
4
+ import { t as ASGProvider } from "./asg-provider-BA2wc324.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
7
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, 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";
@@ -2619,6 +2619,38 @@ function collectCrossStackRefs(node, onRef) {
2619
2619
  //#endregion
2620
2620
  //#region src/provisioning/providers/iam-policy-provider.ts
2621
2621
  /**
2622
+ * The short `ResourceDeleteResult.reason` the no-policy-name DELETE arm
2623
+ * reports (issue [#1770](https://github.com/go-to-k/cdkd/issues/1770)).
2624
+ *
2625
+ * Rendered inline on the destroy status line, so it is the SHORT form; the full
2626
+ * remediation sentence goes out as the `logger.warn` beside it. It says "in
2627
+ * state" rather than "in physicalId" because the arm is only reached once BOTH
2628
+ * sources have been exhausted — the physicalId and `properties['PolicyName']`.
2629
+ */
2630
+ const POLICY_NAME_SKIP_REASON = "no policy name in state — no delete issued";
2631
+ /**
2632
+ * The short `ResourceDeleteResult.reason` for a record that names no principal
2633
+ * to detach the inline policy from (issue
2634
+ * [#1770](https://github.com/go-to-k/cdkd/issues/1770) review).
2635
+ *
2636
+ * Distinct from {@link POLICY_NAME_SKIP_REASON}: the policy may be perfectly
2637
+ * well named and it is the ATTACHMENT that is unknown, so pointing the user at
2638
+ * the physicalId would send them to the wrong half of the record.
2639
+ */
2640
+ const POLICY_NO_TARGET_SKIP_REASON = "no Roles/Groups/Users in state — no delete issued";
2641
+ /**
2642
+ * The deploy-side caveat the skip warning in this file carries (issue
2643
+ * [#1762](https://github.com/go-to-k/cdkd/issues/1762)).
2644
+ *
2645
+ * "Repair state.json and re-run" is only true on DESTROY, where the skip KEEPS
2646
+ * the record. The same arm is ALSO reached from `deploy-engine.ts` and
2647
+ * `rollback-executor.ts`, which discard the delete result and DROP the record —
2648
+ * there the id is gone, so re-running cannot help and the resource has to be
2649
+ * removed by hand. Mirrors the caveat `compositeIdFormatMessage` already
2650
+ * carries for the composite-id family.
2651
+ */
2652
+ const DEPLOY_SKIP_CAVEAT$3 = "NOTE this arm is ALSO reached from cdkd deploy (the DELETE of a resource removed from the template, plus the replacement / rollback deletes), which DROP the state record and report success (https://github.com/go-to-k/cdkd/issues/1762) — there, remove the resource by hand.";
2653
+ /**
2622
2654
  * AWS IAM Policy Provider
2623
2655
  *
2624
2656
  * Implements resource provisioning for AWS::IAM::Policy using the IAM SDK.
@@ -2780,21 +2812,34 @@ var IAMPolicyProvider = class {
2780
2812
  */
2781
2813
  async delete(logicalId, physicalId, resourceType, properties, context) {
2782
2814
  this.logger.debug(`Deleting IAM policy ${logicalId}: ${physicalId}`);
2783
- const policyName = physicalId.includes(":") ? physicalId.split(":")[0] : physicalId;
2815
+ const policyNameFromPhysicalId = physicalId.includes(":") ? physicalId.split(":")[0] : physicalId;
2816
+ const policyNameFromProperties = typeof properties?.["PolicyName"] === "string" ? properties["PolicyName"] : void 0;
2817
+ const policyName = policyNameFromPhysicalId || policyNameFromProperties;
2818
+ const roles = properties?.["Roles"];
2819
+ const groups = properties?.["Groups"];
2820
+ const users = properties?.["Users"];
2821
+ const legacyRoleFromPhysicalId = !roles && !groups && !users && physicalId.includes(":") ? physicalId.split(":")[1] : void 0;
2784
2822
  if (!policyName) {
2785
- this.logger.warn(`Invalid physical ID format: ${physicalId}, skipping deletion`);
2786
- return;
2823
+ this.logger.warn(`Invalid physical ID format: ${physicalId}, and no PolicyName in the state record's properties — skipping deletion. No AWS call is issued, so the inline policy is LEFT ATTACHED to its roles / groups / users, UNLESS the role / group / user it is attached to is itself part of this stack (deleting that principal removes its inline policies, and then only the cdkd record is stale — clear it with 'cdkd state orphan <stack>'). Otherwise repair the physicalId in state.json and re-run, or delete the inline policy by hand. ${DEPLOY_SKIP_CAVEAT$3}`);
2824
+ return {
2825
+ outcome: "skipped",
2826
+ reason: POLICY_NAME_SKIP_REASON
2827
+ };
2828
+ }
2829
+ if (!roles && !groups && !users && !legacyRoleFromPhysicalId) {
2830
+ this.logger.warn(`No Roles, Groups or Users in the state record for IAM policy ${logicalId} (physicalId "${physicalId}"), skipping deletion — an inline policy exists only as an attachment, so with no principal named there is no delete to issue and the policy is LEFT ATTACHED wherever it is, UNLESS the role / group / user it is attached to is itself part of this stack (deleting that principal removes its inline policies, and then only the cdkd record is stale — clear it with 'cdkd state orphan <stack>'). Otherwise restore Roles / Groups / Users in state.json and re-run, or delete the inline policy by hand. ${DEPLOY_SKIP_CAVEAT$3}`);
2831
+ return {
2832
+ outcome: "skipped",
2833
+ reason: POLICY_NO_TARGET_SKIP_REASON
2834
+ };
2787
2835
  }
2788
2836
  const onNotFound = async (target) => {
2789
2837
  assertRegionMatch(await this.iamClient.config.region(), context?.expectedRegion, resourceType, logicalId, `${physicalId} (${target})`);
2790
2838
  };
2791
2839
  try {
2792
- const roles = properties?.["Roles"];
2793
- const groups = properties?.["Groups"];
2794
- const users = properties?.["Users"];
2795
- if (!roles && !groups && !users && physicalId.includes(":")) {
2796
- const firstRole = physicalId.split(":")[1];
2797
- if (firstRole) try {
2840
+ if (legacyRoleFromPhysicalId) {
2841
+ const firstRole = legacyRoleFromPhysicalId;
2842
+ try {
2798
2843
  await this.iamClient.send(new DeleteRolePolicyCommand({
2799
2844
  RoleName: firstRole,
2800
2845
  PolicyName: policyName
@@ -3893,6 +3938,36 @@ var IAMAccessKeyProvider = class {
3893
3938
  //#endregion
3894
3939
  //#region src/provisioning/providers/iam-user-group-provider.ts
3895
3940
  /**
3941
+ * The short `ResourceDeleteResult.reason` the no-properties
3942
+ * `AWS::IAM::UserToGroupAddition` DELETE arm reports (issue
3943
+ * [#1770](https://github.com/go-to-k/cdkd/issues/1770)).
3944
+ *
3945
+ * Rendered inline on the destroy status line, so it is the SHORT form; the full
3946
+ * remediation sentence goes out as the `logger.warn` beside it. It names the
3947
+ * MEMBERSHIP rather than the resource because that is what survives — the
3948
+ * resource itself is metadata, the users staying in the group is the residue.
3949
+ */
3950
+ const MEMBERSHIP_NO_PROPERTIES_SKIP_REASON = "no properties in state — group membership not removed";
3951
+ /**
3952
+ * Sibling of {@link MEMBERSHIP_NO_PROPERTIES_SKIP_REASON} for the arm where the
3953
+ * record has properties but is missing `GroupName` or `Users` — both REQUIRED
3954
+ * by the CloudFormation schema, so their absence is corruption, not an
3955
+ * empty-but-valid membership list.
3956
+ */
3957
+ const MEMBERSHIP_MISSING_FIELDS_SKIP_REASON = "GroupName/Users missing from state — membership not removed";
3958
+ /**
3959
+ * The deploy-side caveat both `UserToGroupAddition` skip warnings carry (issue
3960
+ * [#1762](https://github.com/go-to-k/cdkd/issues/1762)).
3961
+ *
3962
+ * "Repair state.json and re-run" is only true on DESTROY, where the skip KEEPS
3963
+ * the record. The same arms are ALSO reached from `deploy-engine.ts` and
3964
+ * `rollback-executor.ts`, which discard the delete result and DROP the record —
3965
+ * there the id is gone, so re-running cannot help and the memberships have to
3966
+ * be removed by hand. Mirrors the caveat `compositeIdFormatMessage` already
3967
+ * carries for the composite-id family.
3968
+ */
3969
+ const DEPLOY_SKIP_CAVEAT$2 = "NOTE this arm is ALSO reached from cdkd deploy (the DELETE of a resource removed from the template, plus the replacement / rollback deletes), which DROP the state record and report success (https://github.com/go-to-k/cdkd/issues/1762) — there, remove the memberships by hand.";
3970
+ /**
3896
3971
  * AWS IAM User / Group / UserToGroupAddition Provider
3897
3972
  *
3898
3973
  * Implements resource provisioning for:
@@ -4531,14 +4606,20 @@ var IAMUserGroupProvider = class {
4531
4606
  async deleteUserToGroupAddition(logicalId, physicalId, resourceType, properties, _context) {
4532
4607
  this.logger.debug(`Deleting IAM UserToGroupAddition ${logicalId}`);
4533
4608
  if (!properties) {
4534
- this.logger.debug(`No properties for UserToGroupAddition ${logicalId}, skipping deletion`);
4535
- return;
4609
+ this.logger.warn(`No properties for UserToGroupAddition ${logicalId}, skipping deletion — GroupName and Users are both required to call RemoveUserFromGroup, so no AWS call is issued and the group memberships are LEFT IN PLACE, UNLESS the group or the users are themselves part of this stack (their own deletes remove exactly these memberships, and then only the cdkd record is stale — clear it with 'cdkd state orphan <stack>'). Otherwise restore the record's properties in state.json and re-run, or remove the users from the group by hand. ${DEPLOY_SKIP_CAVEAT$2}`);
4610
+ return {
4611
+ outcome: "skipped",
4612
+ reason: MEMBERSHIP_NO_PROPERTIES_SKIP_REASON
4613
+ };
4536
4614
  }
4537
4615
  const groupName = properties["GroupName"];
4538
4616
  const users = properties["Users"];
4539
4617
  if (!groupName || !users) {
4540
- this.logger.debug(`Missing GroupName or Users for ${logicalId}, skipping deletion`);
4541
- return;
4618
+ this.logger.warn(`Missing GroupName or Users for ${logicalId}, skipping deletion — both are required to call RemoveUserFromGroup, so no AWS call is issued and the group memberships are LEFT IN PLACE, UNLESS the group or the users are themselves part of this stack (their own deletes remove exactly these memberships, and then only the cdkd record is stale — clear it with 'cdkd state orphan <stack>'). Otherwise restore them in state.json and re-run, or remove the users from the group by hand. ${DEPLOY_SKIP_CAVEAT$2}`);
4619
+ return {
4620
+ outcome: "skipped",
4621
+ reason: MEMBERSHIP_MISSING_FIELDS_SKIP_REASON
4622
+ };
4542
4623
  }
4543
4624
  try {
4544
4625
  for (const userName of users) try {
@@ -11748,6 +11829,36 @@ var LambdaFunctionProvider = class {
11748
11829
  //#endregion
11749
11830
  //#region src/provisioning/providers/lambda-permission-provider.ts
11750
11831
  /**
11832
+ * The short `ResourceDeleteResult.reason` the missing-`FunctionName` DELETE arm
11833
+ * reports (issue [#1770](https://github.com/go-to-k/cdkd/issues/1770)).
11834
+ *
11835
+ * Rendered inline on the destroy status line, so it is the SHORT form; the full
11836
+ * remediation sentence goes out as the `logger.warn` beside it.
11837
+ */
11838
+ const PERMISSION_FUNCTION_NAME_SKIP_REASON = "FunctionName missing from state — no delete issued";
11839
+ /**
11840
+ * The short `ResourceDeleteResult.reason` for a composite physicalId whose
11841
+ * trailing StatementId segment is empty (`"<functionArn>|"`).
11842
+ *
11843
+ * Its own reason rather than {@link PERMISSION_FUNCTION_NAME_SKIP_REASON}: the
11844
+ * function may be perfectly well named, and pointing the user at FunctionName
11845
+ * would send them to the wrong half of the record.
11846
+ */
11847
+ const PERMISSION_STATEMENT_ID_SKIP_REASON = "empty StatementId in state physicalId — no delete issued";
11848
+ /**
11849
+ * The deploy-side caveat the skip warning in this file carries (issue
11850
+ * [#1762](https://github.com/go-to-k/cdkd/issues/1762)).
11851
+ *
11852
+ * "Repair state.json and re-run" is only true on DESTROY, where the skip KEEPS
11853
+ * the record. The same arm is ALSO reached from `deploy-engine.ts` and
11854
+ * `rollback-executor.ts`, which discard the delete result and DROP the record —
11855
+ * there the id is gone, so re-running cannot help and the resource has to be
11856
+ * removed by hand. Mirrors the caveat `compositeIdFormatMessage` already
11857
+ * carries for the composite-id family; a remedy that is impossible on the path
11858
+ * the user is actually on is worse than no remedy.
11859
+ */
11860
+ const DEPLOY_SKIP_CAVEAT$1 = "NOTE this arm is ALSO reached from cdkd deploy (the DELETE of a resource removed from the template, plus the replacement / rollback deletes), which DROP the state record and report success (https://github.com/go-to-k/cdkd/issues/1762) — there, remove the resource by hand.";
11861
+ /**
11751
11862
  * AWS Lambda Permission Provider
11752
11863
  *
11753
11864
  * Implements resource provisioning for AWS::Lambda::Permission using the Lambda SDK.
@@ -11850,13 +11961,36 @@ var LambdaPermissionProvider = class {
11850
11961
  */
11851
11962
  async delete(logicalId, physicalId, resourceType, properties, context) {
11852
11963
  this.logger.debug(`Deleting Lambda permission ${logicalId}: ${physicalId}`);
11853
- const functionName = properties?.["FunctionName"];
11964
+ const physicalIdSegments = physicalId.split("|");
11965
+ const isComposite = physicalIdSegments.length > 1;
11966
+ const statementId = isComposite ? physicalIdSegments[physicalIdSegments.length - 1] : physicalId;
11967
+ const functionNameFromProperties = typeof properties?.["FunctionName"] === "string" ? properties["FunctionName"] : void 0;
11968
+ let functionNameFromPhysicalId;
11969
+ const leadingSegment = isComposite ? physicalIdSegments[0] : "";
11970
+ if (leadingSegment !== "") {
11971
+ if (!leadingSegment.startsWith("arn:")) functionNameFromPhysicalId = leadingSegment;
11972
+ else if (leadingSegment.includes(":function:")) {
11973
+ const arnRegion = leadingSegment.split(":")[3];
11974
+ const clientRegion = await this.lambdaClient.config.region();
11975
+ if (arnRegion === clientRegion) functionNameFromPhysicalId = leadingSegment;
11976
+ else this.logger.debug(`Lambda permission ${logicalId}: physicalId ARN region ${arnRegion} does not match the client region ${clientRegion}, so it cannot be used as a FunctionName`);
11977
+ }
11978
+ }
11979
+ if (statementId === "") {
11980
+ this.logger.warn(`Lambda permission ${logicalId} has no StatementId in its physicalId ("${physicalId}"), skipping deletion — no AWS call is issued, so the permission statement is LEFT IN PLACE on the function's resource policy, UNLESS the function itself is part of this stack (deleting it removes its whole resource policy, and then only the cdkd record is stale — clear it with 'cdkd state orphan <stack>'). Otherwise repair the physicalId in state.json and re-run, or remove the statement by hand ('aws lambda remove-permission'). ${DEPLOY_SKIP_CAVEAT$1}`);
11981
+ return {
11982
+ outcome: "skipped",
11983
+ reason: PERMISSION_STATEMENT_ID_SKIP_REASON
11984
+ };
11985
+ }
11986
+ const functionName = functionNameFromProperties || functionNameFromPhysicalId;
11854
11987
  if (!functionName) {
11855
- this.logger.warn(`FunctionName not available for Lambda permission ${logicalId}, skipping deletion`);
11856
- return;
11988
+ this.logger.warn(`FunctionName not available for Lambda permission ${logicalId} (neither the state record's FunctionName nor a function ARN in the physicalId), skipping deletion — no AWS call is issued, so the permission statement is LEFT IN PLACE on the function's resource policy, UNLESS the function itself is part of this stack (deleting it removes its whole resource policy, and then only the cdkd record is stale — clear it with 'cdkd state orphan <stack>'). Otherwise repair the record's FunctionName in state.json and re-run, or remove the statement by hand ('aws lambda remove-permission'). ${DEPLOY_SKIP_CAVEAT$1}`);
11989
+ return {
11990
+ outcome: "skipped",
11991
+ reason: PERMISSION_FUNCTION_NAME_SKIP_REASON
11992
+ };
11857
11993
  }
11858
- let statementId = physicalId;
11859
- if (physicalId.includes("|")) statementId = physicalId.split("|").pop();
11860
11994
  try {
11861
11995
  await this.lambdaClient.send(new RemovePermissionCommand({
11862
11996
  FunctionName: functionName,
@@ -12679,6 +12813,36 @@ var LambdaEventSourceMappingProvider = class {
12679
12813
  //#endregion
12680
12814
  //#region src/provisioning/providers/lambda-layer-provider.ts
12681
12815
  /**
12816
+ * The short `ResourceDeleteResult.reason` the two malformed-`LayerVersionArn`
12817
+ * DELETE arms report (issue
12818
+ * [#1770](https://github.com/go-to-k/cdkd/issues/1770)).
12819
+ *
12820
+ * Rendered inline on the destroy status line
12821
+ * (`⚠ MyLayer (AWS::Lambda::LayerVersion) skipped (<reason>)`), so it is the
12822
+ * SHORT form — the full remediation sentence goes out as the `logger.warn`
12823
+ * beside it. Exported so the wording is pinned by a test rather than retyped.
12824
+ */
12825
+ const LAYER_ARN_SKIP_REASON = "malformed LayerVersionArn in state — no delete issued";
12826
+ /**
12827
+ * Sibling of {@link LAYER_ARN_SKIP_REASON} for the arm where the ARN has the
12828
+ * right shape but its trailing version segment is not a number. Kept distinct
12829
+ * because the two point at different halves of the id, and the destroy line is
12830
+ * all the user sees.
12831
+ */
12832
+ const LAYER_VERSION_SKIP_REASON = "unparsable version in state LayerVersionArn — no delete issued";
12833
+ /**
12834
+ * The deploy-side caveat both skip warnings in this file carry (issue
12835
+ * [#1762](https://github.com/go-to-k/cdkd/issues/1762)).
12836
+ *
12837
+ * "Repair state.json and re-run" is only true on DESTROY, where the skip KEEPS
12838
+ * the record. The same arms are ALSO reached from `deploy-engine.ts` and
12839
+ * `rollback-executor.ts`, which discard the delete result and DROP the record —
12840
+ * there the id is gone, so re-running cannot help and the resource has to be
12841
+ * removed by hand. Mirrors the caveat `compositeIdFormatMessage` already
12842
+ * carries for the composite-id family.
12843
+ */
12844
+ const DEPLOY_SKIP_CAVEAT = "NOTE this arm is ALSO reached from cdkd deploy (the DELETE of a resource removed from the template, plus the replacement / rollback deletes), which DROP the state record and report success (https://github.com/go-to-k/cdkd/issues/1762) — there, remove the resource by hand.";
12845
+ /**
12682
12846
  * AWS Lambda LayerVersion Provider
12683
12847
  *
12684
12848
  * Implements resource provisioning for AWS::Lambda::LayerVersion using the Lambda SDK.
@@ -12768,14 +12932,20 @@ var LambdaLayerVersionProvider = class {
12768
12932
  this.logger.debug(`Deleting Lambda layer version ${logicalId}: ${physicalId}`);
12769
12933
  const arnParts = physicalId.split(":");
12770
12934
  if (arnParts.length < 8) {
12771
- this.logger.warn(`Invalid LayerVersionArn format: ${physicalId}, skipping deletion`);
12772
- return;
12935
+ this.logger.warn(`Invalid LayerVersionArn format: ${physicalId}, skipping deletion — no AWS call is issued, so the layer version is LEFT IN PLACE and still counts against the account's storage quota. Repair the physicalId in state.json and re-run, or delete the layer version by hand. ${DEPLOY_SKIP_CAVEAT}`);
12936
+ return {
12937
+ outcome: "skipped",
12938
+ reason: LAYER_ARN_SKIP_REASON
12939
+ };
12773
12940
  }
12774
12941
  const layerName = arnParts[6];
12775
12942
  const versionNumber = parseInt(arnParts[7], 10);
12776
12943
  if (isNaN(versionNumber)) {
12777
- this.logger.warn(`Could not parse version number from ARN: ${physicalId}, skipping deletion`);
12778
- return;
12944
+ this.logger.warn(`Could not parse version number from ARN: ${physicalId}, skipping deletion — no AWS call is issued, so the layer version is LEFT IN PLACE and still counts against the account's storage quota. Repair the physicalId in state.json and re-run, or delete the layer version by hand. ${DEPLOY_SKIP_CAVEAT}`);
12945
+ return {
12946
+ outcome: "skipped",
12947
+ reason: LAYER_VERSION_SKIP_REASON
12948
+ };
12779
12949
  }
12780
12950
  try {
12781
12951
  await this.lambdaClient.send(new DeleteLayerVersionCommand({
@@ -40765,7 +40935,7 @@ var GlueProvider = class {
40765
40935
  }
40766
40936
  async create(logicalId, resourceType, properties, context) {
40767
40937
  switch (resourceType) {
40768
- case "AWS::Glue::Database": return this.createDatabase(logicalId, resourceType, properties);
40938
+ case "AWS::Glue::Database": return this.createDatabase(logicalId, resourceType, properties, context);
40769
40939
  case "AWS::Glue::Table": return this.createTable(logicalId, resourceType, properties, context);
40770
40940
  default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId);
40771
40941
  }
@@ -40784,7 +40954,7 @@ var GlueProvider = class {
40784
40954
  default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId, physicalId);
40785
40955
  }
40786
40956
  }
40787
- async createDatabase(logicalId, resourceType, properties) {
40957
+ async createDatabase(logicalId, resourceType, properties, context) {
40788
40958
  this.logger.debug(`Creating Glue Database ${logicalId}`);
40789
40959
  const databaseInput = properties["DatabaseInput"];
40790
40960
  if (!databaseInput) throw new ProvisioningError(`DatabaseInput is required for Glue Database ${logicalId}`, resourceType, logicalId);
@@ -40794,7 +40964,7 @@ var GlueProvider = class {
40794
40964
  try {
40795
40965
  await this.getClient().send(new CreateDatabaseCommand({
40796
40966
  CatalogId: catalogId,
40797
- DatabaseInput: this.buildDatabaseInput(databaseInput, databaseName)
40967
+ DatabaseInput: this.buildDatabaseInput(databaseInput, databaseName, { ...replayWarn(this.logger, context) })
40798
40968
  }));
40799
40969
  this.logger.debug(`Successfully created Glue Database ${logicalId}: ${databaseName}`);
40800
40970
  return {
@@ -40812,9 +40982,13 @@ var GlueProvider = class {
40812
40982
  if (!databaseInput) throw new ProvisioningError(`DatabaseInput is required for Glue Database update ${logicalId}`, resourceType, logicalId, physicalId);
40813
40983
  const catalogId = properties["CatalogId"];
40814
40984
  const liveParameters = await this.readLiveDatabaseParameters(logicalId, resourceType, physicalId, catalogId);
40985
+ const previousDatabaseInput = asRecord(previousProperties?.["DatabaseInput"]);
40815
40986
  try {
40816
- const builtDatabaseInput = this.buildDatabaseInput(databaseInput, physicalId);
40817
- this.preserveAwsManagedParameters(builtDatabaseInput, databaseInput["Parameters"], (previousProperties?.["DatabaseInput"])?.["Parameters"], liveParameters);
40987
+ const builtDatabaseInput = this.buildDatabaseInput(databaseInput, physicalId, {
40988
+ onUnusable: (message) => this.logger.warn(message),
40989
+ previousDatabaseInput
40990
+ });
40991
+ this.preserveAwsManagedParameters(builtDatabaseInput, databaseInput["Parameters"], previousDatabaseInput?.["Parameters"], liveParameters);
40818
40992
  await this.getClient().send(new UpdateDatabaseCommand({
40819
40993
  ...catalogId !== void 0 && { CatalogId: catalogId },
40820
40994
  Name: physicalId,
@@ -41188,15 +41362,137 @@ var GlueProvider = class {
41188
41362
  * so empty-string Description, empty Parameters map, etc. reach AWS
41189
41363
  * intact — `cdkd drift --revert` relies on this to clear console-side
41190
41364
  * additions.
41191
- */
41192
- buildDatabaseInput(databaseInput, fallbackName) {
41365
+ *
41366
+ * `options` carries the shape-guard policy for the three nested blocks added
41367
+ * by issue #1807, and the two call sites answer differently because their
41368
+ * desired bag has a different provenance (`.claude/rules/providers.md`):
41369
+ *
41370
+ * - CREATE passes NO `onUnusable`, so a malformed block is REFUSED before
41371
+ * the AWS call — the value is template-borne, and the alternative is
41372
+ * sending an empty `{}` block AWS would reject with a far less
41373
+ * actionable message. Under `CreateContext.replayingState` the refusal
41374
+ * downgrades through the shared `replayWarn`, since a reverse-replacement
41375
+ * re-create reads a STATE record the user cannot edit from the template.
41376
+ * - UPDATE passes a warn callback UNCONDITIONALLY (the `updateRoute`
41377
+ * precedent — `update()` has no context, so it cannot tell a template
41378
+ * push from the state-borne bag `drift --revert` and the rollback revert
41379
+ * arm hand it), and then RETAINS the PREVIOUS side's block, which is why
41380
+ * `previousDatabaseInput` is threaded in. Omitting instead would ERASE a
41381
+ * live resource link, because `UpdateDatabase` replaces `DatabaseInput`
41382
+ * wholesale — the #1612 "UPDATE retains the previous value" row, reached
41383
+ * here through a whole-blob API rather than a per-item one. When the
41384
+ * previous side is unusable too the key is DROPPED: there is no value
41385
+ * cdkd can vouch for either way (#1653 review).
41386
+ */
41387
+ buildDatabaseInput(databaseInput, fallbackName, options) {
41388
+ const guardOptions = options?.onUnusable ? { onUnusable: options.onUnusable } : void 0;
41389
+ const previous = options?.previousDatabaseInput;
41390
+ /**
41391
+ * The record a nested block is BUILT FROM: the desired side when it names
41392
+ * at least one sendable member, else the previous side when IT does, else
41393
+ * nothing.
41394
+ *
41395
+ * The emptiness test is what makes the ladder complete, and getting it
41396
+ * wrong is how the first cut of this guard still erased a live resource
41397
+ * link: a bare `{}` and an unresolved `{Ref: ...}` are plain OBJECTS, so
41398
+ * the shape guard accepts them and a `?? previous` fallback never fires —
41399
+ * the block then built empty and `UpdateDatabase`, which replaces
41400
+ * `DatabaseInput` wholesale, dropped a link the template still declared.
41401
+ *
41402
+ * A member counts as sendable only when it is a STRING or a NUMBER: the
41403
+ * leaf casts below are unchecked, so an intrinsic-valued `DatabaseName`
41404
+ * would otherwise put an OBJECT on the wire — the same defect one level in.
41405
+ */
41406
+ const pickBlockSource = (desiredRaw, previousRaw, path, block, members) => {
41407
+ const namesSendable = (bag) => {
41408
+ if (bag === void 0) return false;
41409
+ const present = members.filter((m) => bag[m] !== void 0);
41410
+ return present.length > 0 && present.every((m) => isSendableLeaf(bag[m]));
41411
+ };
41412
+ const desired = guardOptions ? requireConfigObject(desiredRaw, path, guardOptions) : requireConfigObject(desiredRaw, path);
41413
+ if (namesSendable(desired)) return desired;
41414
+ if (desired !== void 0) {
41415
+ const message = emptyBlockMessage(block);
41416
+ if (options?.onUnusable) options.onUnusable(message);
41417
+ else throw new Error(message);
41418
+ }
41419
+ const retained = asRecord(previousRaw);
41420
+ return namesSendable(retained) ? retained : void 0;
41421
+ };
41193
41422
  const result = { Name: databaseInput["Name"] ?? fallbackName };
41194
41423
  if (databaseInput["Description"] !== void 0) result.Description = databaseInput["Description"];
41195
41424
  if (databaseInput["LocationUri"] !== void 0) result.LocationUri = databaseInput["LocationUri"];
41196
41425
  if (databaseInput["Parameters"] !== void 0) result.Parameters = stringifyParameterValues(databaseInput["Parameters"]);
41426
+ if (databaseInput["TargetDatabase"] !== void 0) {
41427
+ const target = pickBlockSource(databaseInput["TargetDatabase"], previous?.["TargetDatabase"], "AWS::Glue::Database DatabaseInput.TargetDatabase", "TargetDatabase", TARGET_DATABASE_MEMBERS);
41428
+ if (target !== void 0) {
41429
+ const targetDatabase = {};
41430
+ if (isSendableLeaf(target["CatalogId"])) targetDatabase.CatalogId = String(target["CatalogId"]);
41431
+ if (isSendableLeaf(target["DatabaseName"])) targetDatabase.DatabaseName = String(target["DatabaseName"]);
41432
+ if (isSendableLeaf(target["Region"])) targetDatabase.Region = String(target["Region"]);
41433
+ result.TargetDatabase = targetDatabase;
41434
+ }
41435
+ }
41436
+ if (databaseInput["FederatedDatabase"] !== void 0) {
41437
+ const federated = pickBlockSource(databaseInput["FederatedDatabase"], previous?.["FederatedDatabase"], "AWS::Glue::Database DatabaseInput.FederatedDatabase", "FederatedDatabase", FEDERATED_DATABASE_MEMBERS);
41438
+ if (federated !== void 0) {
41439
+ const federatedDatabase = {};
41440
+ if (isSendableLeaf(federated["ConnectionName"])) federatedDatabase.ConnectionName = String(federated["ConnectionName"]);
41441
+ if (isSendableLeaf(federated["Identifier"])) federatedDatabase.Identifier = String(federated["Identifier"]);
41442
+ result.FederatedDatabase = federatedDatabase;
41443
+ }
41444
+ }
41445
+ if (databaseInput["CreateTableDefaultPermissions"] !== void 0) {
41446
+ const permissions = this.usableDefaultPermissions(databaseInput["CreateTableDefaultPermissions"], guardOptions) ?? this.usableDefaultPermissions(previous?.["CreateTableDefaultPermissions"], { onUnusable: () => void 0 });
41447
+ if (permissions !== void 0) result.CreateTableDefaultPermissions = permissions.map((entry) => {
41448
+ const permission = {};
41449
+ if (entry["Permissions"] !== void 0) permission.Permissions = entry["Permissions"];
41450
+ const declaredPrincipal = asRecord(entry["Principal"]);
41451
+ if (declaredPrincipal !== void 0) {
41452
+ const principal = {};
41453
+ if (isSendableLeaf(declaredPrincipal["DataLakePrincipalIdentifier"])) principal.DataLakePrincipalIdentifier = String(declaredPrincipal["DataLakePrincipalIdentifier"]);
41454
+ permission.Principal = principal;
41455
+ }
41456
+ return permission;
41457
+ });
41458
+ }
41197
41459
  return result;
41198
41460
  }
41199
41461
  /**
41462
+ * `DatabaseInput.CreateTableDefaultPermissions` as a USABLE list, or
41463
+ * `undefined` when the declared value is a shape cdkd refuses to narrow.
41464
+ *
41465
+ * The block is validated ALL-OR-NOTHING rather than per entry, which is the
41466
+ * opposite of the S3 per-`Id` appliers and deliberate: this is a Lake
41467
+ * Formation grant list delivered by ONE wholesale `CreateDatabase` /
41468
+ * `UpdateDatabase` call, so dropping the entries cdkd cannot read would
41469
+ * silently NARROW the permissions the template declared — the failure this
41470
+ * guard exists to prevent, one level in from the block itself. A malformed
41471
+ * ENTRY therefore invalidates the block, and the caller then refuses (create)
41472
+ * or retains the previous list (update).
41473
+ *
41474
+ * `Permissions` is checked for ARRAY-ness for the same reason: the SDK member
41475
+ * is `Permission[]`, so a bare string would be forwarded verbatim and the
41476
+ * serializer would drop it, granting nothing while the deploy reports success.
41477
+ */
41478
+ usableDefaultPermissions(value, guardOptions) {
41479
+ if (value === void 0) return void 0;
41480
+ const path = "AWS::Glue::Database DatabaseInput.CreateTableDefaultPermissions";
41481
+ const entries = guardOptions ? requireConfigArray(value, path, guardOptions) : requireConfigArray(value, path);
41482
+ if (entries === void 0) return void 0;
41483
+ for (const entry of entries) {
41484
+ const principal = asRecord(entry["Principal"]);
41485
+ if (asRecord(entry) !== void 0 && (entry["Permissions"] !== void 0 || entry["Principal"] !== void 0) && (entry["Permissions"] === void 0 || Array.isArray(entry["Permissions"])) && (entry["Principal"] === void 0 || principal !== void 0 && isSendableLeaf(principal["DataLakePrincipalIdentifier"]))) continue;
41486
+ const detail = `${path} carries an entry cdkd cannot read (each entry must be an object naming Permissions as a list and/or Principal as an object whose DataLakePrincipalIdentifier is a string or a number)`;
41487
+ if (guardOptions?.onUnusable) {
41488
+ guardOptions.onUnusable(`${detail}. Leaving the whole block unapplied rather than sending a NARROWED grant list. On an UPDATE that is a RESET: UpdateDatabase replaces DatabaseInput wholesale, so unless a usable previous list is retained the database falls back to the account default (IAM_ALLOWED_PRINCIPALS / ALL) — fix the template value to restore the declared grants`);
41489
+ return;
41490
+ }
41491
+ throw new Error(detail);
41492
+ }
41493
+ return entries;
41494
+ }
41495
+ /**
41200
41496
  * Build TableInput for Glue API from CFn template properties
41201
41497
  */
41202
41498
  buildTableInput(tableInput, fallbackName) {
@@ -41306,9 +41602,30 @@ var GlueProvider = class {
41306
41602
  * readback for it (and fabricating a placeholder would itself fire false
41307
41603
  * drift). Declaring it here keeps the drift comparator from false-positiving
41308
41604
  * on a state-recorded `OpenTableFormatInput` that the readback never surfaces.
41605
+ *
41606
+ * `AWS::Glue::Database` declares `DatabaseInput.CreateTableDefaultPermissions`
41607
+ * PER RESOURCE (the issue #1602 seam), and only for a database whose template
41608
+ * does NOT declare the block. AWS materializes a Lake Formation DEFAULT there
41609
+ * — `[{IAM_ALLOWED_PRINCIPALS, [ALL]}]` on a catalog that has not been
41610
+ * onboarded to Lake Formation (measured us-east-1, 2026-08-13) — so once
41611
+ * issue #1807 taught `readDatabase` to surface the block, comparing it would
41612
+ * report drift on EVERY existing database: the baseline is an
41613
+ * `observedProperties` capture written by a binary that did not read the key,
41614
+ * `drift.ts` walks the key UNION on that baseline (`unionWalkObjects`), and
41615
+ * `deploy-engine.ts` does not refresh a record that already has one — so the
41616
+ * phantom would persist until an unrelated UPDATE or a `drift --accept`.
41617
+ * This is the ELBv2 `Targets` shape one type over: a value a SIBLING owns is
41618
+ * ignored while the template declares none, and an explicit declaration —
41619
+ * including an empty list — IS a declaration and stays compared.
41620
+ *
41621
+ * `TargetDatabase` / `FederatedDatabase` need no such scoping: AWS reports
41622
+ * them only when they were actually set, so an appearance is real drift.
41309
41623
  */
41310
- getDriftUnknownPaths(resourceType) {
41624
+ getDriftUnknownPaths(resourceType, properties) {
41311
41625
  if (resourceType === "AWS::Glue::Table") return ["OpenTableFormatInput"];
41626
+ if (resourceType === "AWS::Glue::Database") {
41627
+ if (asRecord(properties?.["DatabaseInput"])?.["CreateTableDefaultPermissions"] === void 0) return ["DatabaseInput.CreateTableDefaultPermissions"];
41628
+ }
41312
41629
  return [];
41313
41630
  }
41314
41631
  async readDatabase(physicalId, catalogId) {
@@ -41328,6 +41645,9 @@ var GlueProvider = class {
41328
41645
  dbInput["Description"] = db.Description ?? "";
41329
41646
  if (db.LocationUri !== void 0) dbInput["LocationUri"] = db.LocationUri;
41330
41647
  dbInput["Parameters"] = db.Parameters ?? {};
41648
+ if (db.TargetDatabase !== void 0) dbInput["TargetDatabase"] = { ...db.TargetDatabase };
41649
+ if (db.FederatedDatabase !== void 0) dbInput["FederatedDatabase"] = { ...db.FederatedDatabase };
41650
+ if (db.CreateTableDefaultPermissions !== void 0) dbInput["CreateTableDefaultPermissions"] = db.CreateTableDefaultPermissions.map((entry) => ({ ...entry }));
41331
41651
  const result = { DatabaseInput: dbInput };
41332
41652
  if (db.Name !== void 0) result["DatabaseName"] = db.Name;
41333
41653
  return result;
@@ -42210,6 +42530,35 @@ function parameterKeySet(value) {
42210
42530
  * its `Fn::*`/`Ref` key — harmless here, since providers only ever see
42211
42531
  * resolved values.)
42212
42532
  */
42533
+ /**
42534
+ * The CFn members of the two `DatabaseInput` object blocks, as the SENDABLE-ness
42535
+ * test sees them. They must stay in step with the per-member writes in
42536
+ * `buildDatabaseInput` — a name here that the builder does not write would let
42537
+ * an EMPTY block onto the wire, and one the builder writes but this list omits
42538
+ * would refuse a valid template that names only it. The unit suite fences both
42539
+ * directions by asserting each member ALONE reaches AWS, which fails on a
42540
+ * dropped entry (the member stops being sendable) and on an added one (the
42541
+ * block builds empty).
42542
+ */
42543
+ const TARGET_DATABASE_MEMBERS = [
42544
+ "CatalogId",
42545
+ "DatabaseName",
42546
+ "Region"
42547
+ ];
42548
+ const FEDERATED_DATABASE_MEMBERS = ["ConnectionName", "Identifier"];
42549
+ /**
42550
+ * Is this leaf value one cdkd can put on the wire? A STRING or a NUMBER — CFn
42551
+ * is stringly typed and an unquoted YAML account id arrives as a number (the
42552
+ * reason `catalogIdForApi` exists) — and nothing else, so an unresolved
42553
+ * intrinsic cannot ride an unchecked cast into the request.
42554
+ */
42555
+ const isSendableLeaf = (value) => typeof value === "string" || typeof value === "number" && Number.isFinite(value);
42556
+ /**
42557
+ * The refusal sentence for a `DatabaseInput` block that IS a plain object and
42558
+ * still names nothing cdkd can send — an unresolved intrinsic or a bare `{}`.
42559
+ * Shared so the create refusal and the update warning cannot drift apart.
42560
+ */
42561
+ const emptyBlockMessage = (block) => `AWS::Glue::Database DatabaseInput.${block} declares no member cdkd can send (an unresolved intrinsic, an empty block), or mixes sendable members with unreadable ones; sending it would put an empty or NARROWED ${block} on the wire. Where this is only WARNED — an update, or a state replay — the previously applied block is retained if cdkd can still read one, and otherwise the key is omitted, which UpdateDatabase applies as a removal because it replaces DatabaseInput wholesale`;
42213
42562
  function asRecord(value) {
42214
42563
  if (typeof value !== "object" || value === null || Array.isArray(value)) return;
42215
42564
  return value;
@@ -59623,6 +59972,27 @@ const COMPOSITE_ID_SPLITTERS = {
59623
59972
  propertiesOverlay: { VpcId: vpcId }
59624
59973
  };
59625
59974
  },
59975
+ "AWS::EC2::VPCCidrBlock": (physicalId, properties) => {
59976
+ if (!physicalId.trim()) throw new Error("empty physical id for AWS::EC2::VPCCidrBlock (expected an association id)");
59977
+ const parts = physicalId.split("|").map((p) => p.trim());
59978
+ if (parts.length > 2) throw new Error(`expected a bare '<Id>' or '<Id>|<VpcId>', got ${parts.length} parts: '${physicalId}'`);
59979
+ const id = parts[0];
59980
+ if (!id) throw new Error(`empty part in '<Id>|<VpcId>': '${physicalId}'`);
59981
+ if (!id.startsWith(VPC_CIDR_ASSOCIATION_ID_PREFIX)) throw new Error(`'${physicalId}' does not start with an association id: CloudFormation identifies an AWS::EC2::VPCCidrBlock by [Id, VpcId], so the first segment must be a '${VPC_CIDR_ASSOCIATION_ID_PREFIX}…' id. State entry may be corrupt, or the segments may be reversed; re-deploy the resource to refresh state`);
59982
+ let vpcId;
59983
+ if (parts.length === 2) {
59984
+ vpcId = parts[1];
59985
+ if (!vpcId) throw new Error(`empty part in '<Id>|<VpcId>': '${physicalId}'`);
59986
+ } else vpcId = readStringProperty(properties, "VpcId", "AWS::EC2::VPCCidrBlock");
59987
+ if (!VPC_ID_PATTERN.test(vpcId)) throw new Error(`'${vpcId}' is not a VPC id (expected 'vpc-…', not an association id): '${physicalId}' may have its segments reversed`);
59988
+ return {
59989
+ resourceIdentifier: {
59990
+ Id: id,
59991
+ VpcId: vpcId
59992
+ },
59993
+ propertiesOverlay: { VpcId: vpcId }
59994
+ };
59995
+ },
59626
59996
  "AWS::EC2::Route": (physicalId, properties) => {
59627
59997
  const parts = physicalId.split("|");
59628
59998
  if (parts.length !== 2) throw new Error(`expected 2 parts ('<routeTableId>|<destination>'), got ${parts.length}: '${physicalId}'`);
@@ -59752,6 +60122,26 @@ const EIP_ALLOCATION_ID_PREFIX = "eipalloc-";
59752
60122
  * validate an address AWS itself minted.
59753
60123
  */
59754
60124
  const EIP_PUBLIC_IP_PATTERN = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
60125
+ /**
60126
+ * The prefix every `AWS::EC2::VPCCidrBlock` association id carries. The same
60127
+ * kind of shape discriminator as {@link EIP_ALLOCATION_ID_PREFIX}: a VPC id and
60128
+ * an association id BOTH start with `vpc-`, so the longer prefix is what tells
60129
+ * the two segments apart.
60130
+ */
60131
+ const VPC_CIDR_ASSOCIATION_ID_PREFIX = "vpc-cidr-assoc-";
60132
+ /**
60133
+ * The other half of that discriminator. ANCHORED, so an association id
60134
+ * (`vpc-cidr-assoc-…`, which also begins `vpc-`) does NOT match — a reversed
60135
+ * pair is exactly what this exists to catch.
60136
+ *
60137
+ * The character class is `[0-9a-z]` rather than hex, and the LENGTH is not
60138
+ * pinned. EC2 ids are hex today, but `cdkd export` is all-or-nothing, so a
60139
+ * false REFUSAL here blocks an entire export — and the discrimination does not
60140
+ * depend on the alphabet at all: it is the `-` in `cidr-assoc`, which no
60141
+ * character class here admits. Widening the class therefore costs nothing and
60142
+ * removes a false-refusal vector.
60143
+ */
60144
+ const VPC_ID_PATTERN = /^vpc-[0-9a-z]+$/;
59755
60145
  const GATEWAY_ATTACHMENT_TYPE_IGW = "IGW";
59756
60146
  const GATEWAY_ATTACHMENT_TYPE_VGW = "VGW";
59757
60147
  /**
@@ -60347,8 +60737,8 @@ async function exportCommand(stackArg, options) {
60347
60737
  });
60348
60738
  if (blocked.length > 0) {
60349
60739
  logger.error("The following resources block migration:");
60350
- for (const b of blocked) logger.error(` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
60351
- throw new Error(`${blocked.length} resource(s) block migration. Either destroy them first (cdkd destroy / cdkd state destroy cherry-picked), or remove them from the CDK app and re-synthesize.`);
60740
+ for (const line of groupBlockedReasons(blocked)) logger.error(line);
60741
+ throw new Error(`${new Set(blocked.map((b) => b.logicalId)).size} resource(s) block migration. Either destroy them first (cdkd destroy / cdkd state destroy cherry-picked), or remove them from the CDK app and re-synthesize.`);
60352
60742
  }
60353
60743
  const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
60354
60744
  if (!options.dryRun) {
@@ -60955,12 +61345,25 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
60955
61345
  });
60956
61346
  continue;
60957
61347
  }
61348
+ const propertiesOverlay = resolved.propertiesOverlay ?? resolved.resourceIdentifier;
61349
+ const unrepresentable = unrepresentableOverlayFields(raw, propertiesOverlay);
61350
+ if (unrepresentable.length > 0) {
61351
+ for (const u of unrepresentable) blocked.push({
61352
+ logicalId,
61353
+ resourceType,
61354
+ reason: unrepresentableOverlayMessage({
61355
+ logicalId,
61356
+ resourceType
61357
+ }, u.field, u.current, u.value)
61358
+ });
61359
+ continue;
61360
+ }
60958
61361
  phase1Imports.push({
60959
61362
  logicalId,
60960
61363
  resourceType,
60961
61364
  physicalId: stateEntry.physicalId,
60962
61365
  resourceIdentifier: resolved.resourceIdentifier,
60963
- propertiesOverlay: resolved.propertiesOverlay ?? resolved.resourceIdentifier
61366
+ propertiesOverlay
60964
61367
  });
60965
61368
  }
60966
61369
  return {
@@ -60972,6 +61375,28 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
60972
61375
  };
60973
61376
  }
60974
61377
  /**
61378
+ * Render the `blocked` list one bullet per RESOURCE, folding a resource's
61379
+ * several reasons into it.
61380
+ *
61381
+ * The pre-flight pushes one entry per offending FIELD (issue #1787), so a
61382
+ * multi-field overlay such as `AWS::S3Tables::Namespace` contributes two rows
61383
+ * for one resource — which made the count ("1 resource(s)") disagree with the
61384
+ * bullets printed under it. Grouping keeps the two consistent without losing a
61385
+ * reason.
61386
+ */
61387
+ function groupBlockedReasons(blocked) {
61388
+ const byResource = /* @__PURE__ */ new Map();
61389
+ for (const b of blocked) {
61390
+ const entry = byResource.get(b.logicalId) ?? {
61391
+ resourceType: b.resourceType,
61392
+ reasons: []
61393
+ };
61394
+ entry.reasons.push(b.reason);
61395
+ byResource.set(b.logicalId, entry);
61396
+ }
61397
+ return [...byResource].map(([logicalId, { resourceType, reasons }]) => reasons.length === 1 ? ` - ${logicalId} (${resourceType}): ${reasons[0]}` : ` - ${logicalId} (${resourceType}):\n${reasons.map((r) => ` - ${r}`).join("\n")}`);
61398
+ }
61399
+ /**
60975
61400
  * Build the `ResourceIdentifier` map CloudFormation IMPORT expects in
60976
61401
  * `ResourcesToImport[].ResourceIdentifier` for the given resource type
60977
61402
  * and cdkd state's physical ID.
@@ -61230,14 +61655,14 @@ function injectDeletionPolicyForImport(template) {
61230
61655
  }
61231
61656
  return injected;
61232
61657
  }
61233
- function filterTemplateForImport(template, plan) {
61658
+ function filterTemplateForImport(template, plan, warnOnRewrite = true) {
61234
61659
  const allow = new Map(plan.map((p) => [p.logicalId, p]));
61235
61660
  const original = template["Resources"];
61236
61661
  const filteredResources = {};
61237
61662
  for (const [logicalId, resource] of Object.entries(original)) {
61238
61663
  const entry = allow.get(logicalId);
61239
61664
  if (!entry) continue;
61240
- filteredResources[logicalId] = overlayResourceIdentifierOnProperties(resource, entry);
61665
+ filteredResources[logicalId] = overlayResourceIdentifierOnProperties(resource, entry, warnOnRewrite);
61241
61666
  }
61242
61667
  const result = {
61243
61668
  ...template,
@@ -61264,8 +61689,10 @@ function filterTemplateForImport(template, plan) {
61264
61689
  * migration" value proposition. Closes [#319].
61265
61690
  *
61266
61691
  * **Post-v0.95 behavior** (this function): override only when synth
61267
- * already carries a *literal-string* value for the field AND it differs
61268
- * from `ResourceIdentifier`. Three cases:
61692
+ * already carries a *literal* value for the field AND it differs
61693
+ * from `ResourceIdentifier`. Four cases (the fourth added by issue #1787,
61694
+ * which also widened "literal" past `typeof === 'string'` — see
61695
+ * {@link classifyOverlayCurrentValue} for the full classification):
61269
61696
  *
61270
61697
  * - **Absent** (auto-generated names — user did NOT declare a physical
61271
61698
  * name in CDK code): `Properties[field]` stays absent. Matches
@@ -61302,12 +61729,19 @@ function filterTemplateForImport(template, plan) {
61302
61729
  * names` flip are no longer in this case — Properties.RoleName matches
61303
61730
  * the AWS name without override.
61304
61731
  *
61732
+ * - **Unrepresentable** (a list carrying an intrinsic / a nested list / an
61733
+ * empty list): REFUSED with a message naming the resource, the property and
61734
+ * the scalar to declare instead. Preserving ANY of the three reproduces the
61735
+ * opaque `CreateChangeSet` rejection; what makes OVERRIDING wrong differs per
61736
+ * shape, so {@link refusalReason} says which rather than asserting one cause
61737
+ * for all — only a list actually carrying an object element has an intrinsic
61738
+ * to discard. Either way the command stops before submitting anything
61739
+ * (issue #1787).
61740
+ *
61305
61741
  * For literal-match (Properties already has the right value), the
61306
- * override is a no-op, so the check is effectively
61307
- * `Properties[field] !== overlayValue && typeof Properties[field] ===
61308
- * 'string'`.
61742
+ * override is a no-op.
61309
61743
  */
61310
- function overlayResourceIdentifierOnProperties(resource, entry) {
61744
+ function overlayResourceIdentifierOnProperties(resource, entry, warnOnRewrite = true) {
61311
61745
  if (!resource || typeof resource !== "object" || Array.isArray(resource)) return resource;
61312
61746
  const r = resource;
61313
61747
  const existingProperties = r["Properties"];
@@ -61315,7 +61749,13 @@ function overlayResourceIdentifierOnProperties(resource, entry) {
61315
61749
  const overlay = entry.propertiesOverlay ?? entry.resourceIdentifier;
61316
61750
  for (const [field, value] of Object.entries(overlay)) {
61317
61751
  const current = properties[field];
61318
- if (typeof current === "string" && current !== value) properties[field] = value;
61752
+ const kind = classifyOverlayCurrentValue(current);
61753
+ if (kind === "intrinsic" || kind === "absent") continue;
61754
+ if (kind === "unrepresentable") throw new Error(unrepresentableOverlayMessage(entry, field, current, value));
61755
+ if (current !== value) {
61756
+ if (warnOnRewrite && typeof current !== "string") getLogger().warn(`${entry.logicalId} (${entry.resourceType}): rewriting the identifier property '${field}' from ${describeOverlayValueShape(current)} to the scalar '${value}' recorded in cdkd state. CloudFormation types this property as a string; the exported template will carry the scalar.`);
61757
+ properties[field] = value;
61758
+ }
61319
61759
  }
61320
61760
  return {
61321
61761
  ...r,
@@ -61323,6 +61763,133 @@ function overlayResourceIdentifierOnProperties(resource, entry) {
61323
61763
  };
61324
61764
  }
61325
61765
  /**
61766
+ * Why a given `unrepresentable` value is refused rather than rewritten.
61767
+ *
61768
+ * THREE shapes reach `unrepresentable`, and they are refused for two different
61769
+ * reasons — an earlier revision of this message asserted the intrinsic reason
61770
+ * for all of them, which is false for two of the three. The distinction is not
61771
+ * cosmetic: a message that names a cause the value does not have sends the user
61772
+ * looking for an intrinsic that is not there.
61773
+ */
61774
+ function refusalReason(current) {
61775
+ if (Array.isArray(current) && current.length === 0) return "cdkd will not invent a scalar where the template declared no value";
61776
+ return Array.isArray(current) && current.some((e) => e !== null && typeof e === "object" && !Array.isArray(e)) ? "cdkd will not rewrite it, because doing so would discard the intrinsic it carries" : "cdkd will not replace a value it cannot represent as the scalar the schema declares";
61777
+ }
61778
+ /**
61779
+ * The shared message for a refused overlay value — used by the pre-flight in
61780
+ * {@link buildImportPlan} (which BLOCKS the resource) and by
61781
+ * {@link overlayResourceIdentifierOnProperties}'s defense-in-depth throw, so
61782
+ * the two cannot drift into describing the same defect differently.
61783
+ */
61784
+ function unrepresentableOverlayMessage(entry, field, current, value) {
61785
+ return `${entry.logicalId} (${entry.resourceType}): the identifier property '${field}' is ${describeOverlayValueShape(current)}, but CloudFormation types it as a string. ${refusalReason(current)}. Declare '${field}' as the scalar '${value}' (or drop the addPropertyOverride that made it a list) and re-run 'cdkd export'.`;
61786
+ }
61787
+ /**
61788
+ * Every overlay field of `entry` whose value in `resource`'s `Properties` is
61789
+ * {@link classifyOverlayCurrentValue}-`unrepresentable`.
61790
+ *
61791
+ * Exists so the refusal happens during PLANNING rather than during template
61792
+ * preprocessing. `overlayResourceIdentifierOnProperties` is called from three
61793
+ * places, and two of them are past the point of no return: `--dry-run` returns
61794
+ * BEFORE any overlay call (so a dry run would report a clean plan for a
61795
+ * template the real run aborts on), and the nested-stack path calls it inside
61796
+ * the per-stack IMPORT loop — where a throw on stack N leaves stacks 1..N-1
61797
+ * already imported, outside the loop's own "stacks imported so far" recovery
61798
+ * handler. Running the same classification in `buildImportPlan` puts the
61799
+ * verdict where every other unfixable-resource verdict already lives: reported
61800
+ * for EVERY offender at once, before the lock, before dry-run's return, and
61801
+ * before any AWS write.
61802
+ */
61803
+ function unrepresentableOverlayFields(resource, overlay) {
61804
+ if (!resource || typeof resource !== "object" || Array.isArray(resource)) return [];
61805
+ const props = resource["Properties"];
61806
+ if (!props || typeof props !== "object" || Array.isArray(props)) return [];
61807
+ const bag = props;
61808
+ const out = [];
61809
+ for (const [field, value] of Object.entries(overlay)) if (classifyOverlayCurrentValue(bag[field]) === "unrepresentable") out.push({
61810
+ field,
61811
+ current: bag[field],
61812
+ value
61813
+ });
61814
+ return out;
61815
+ }
61816
+ /**
61817
+ * Classify what the synth template currently carries at an identifier field,
61818
+ * for {@link overlayResourceIdentifierOnProperties} (issue #1787).
61819
+ *
61820
+ * The pre-#1787 rule was `typeof current === 'string'`, which is correct for
61821
+ * the two cases it was designed for (an absent key, and a `Ref` / `Fn::GetAtt`
61822
+ * intrinsic that must be preserved — issue #319) and WRONG for a third,
61823
+ * reachable case: a field the template carries as a non-string LITERAL. The
61824
+ * measured instance is `AWS::S3Tables::Namespace`, whose CFn registry type is
61825
+ * `{"type": "string"}` and whose CDK L1 emits a string, but
61826
+ * `addPropertyOverride('Namespace', ['analytics'])` synthesizes
61827
+ * `"Namespace": ["analytics"]` — which cdkd deploys, because
61828
+ * `s3-tables-provider.ts` accepts both wire shapes. The array is not a literal
61829
+ * string, so the overlay left it alone and it reached `CreateChangeSet`, where
61830
+ * CFn answered with an opaque rejection.
61831
+ *
61832
+ * The classification, and why each arm is what it is:
61833
+ *
61834
+ * - `absent` (`undefined` only) — unchanged. An auto-generated name the user
61835
+ * never declared; CFn accepts the IMPORT on `ResourceIdentifier` alone. An
61836
+ * explicit `null` is deliberately NOT here: the key IS present, the shallow
61837
+ * Properties copy keeps it, and `"Namespace": null` then reaches
61838
+ * `CreateChangeSet` — the same opaque-rejection class as the array, through
61839
+ * the same `addPropertyOverride` hatch. It is a literal, and overwritten.
61840
+ * - `intrinsic` (a non-array object) — unchanged. `{Ref: Parent}` /
61841
+ * `{Fn::GetAtt: [...]}` resolve during changeset processing to exactly this
61842
+ * resource's `ResourceIdentifier` value, so clobbering them would only make
61843
+ * the post-export `cdk diff` dirty (issue #319).
61844
+ * - `literal` (string / number / boolean, or an array of those) — OVERRIDDEN
61845
+ * with the overlay value. This is not a guess: the overlay value comes from
61846
+ * the cdkd-recorded physicalId, which is the authority on what the resource
61847
+ * IS, while the template side is only what CDK happened to synthesize. So a
61848
+ * single-element `['analytics']` and a bare `analytics` both become the
61849
+ * scalar CFn's schema declares, exactly as the pre-existing prefixed-name
61850
+ * literal-mismatch case already does.
61851
+ * - `unrepresentable` (an array containing an object / array / null, or an
61852
+ * EMPTY array) — REFUSED. Preserving any of them ships the same opaque CFn
61853
+ * rejection this change exists to remove; the reason OVERRIDING is wrong
61854
+ * differs by shape, and {@link refusalReason} splits it three ways rather
61855
+ * than asserting one cause:
61856
+ * - a list carrying an OBJECT element — overriding discards an intrinsic
61857
+ * the user wrote;
61858
+ * - a NESTED list, or one holding `null` — no intrinsic to protect, it is
61859
+ * simply not representable as the declared scalar;
61860
+ * - an EMPTY list — nothing to reconcile against the identifier, and
61861
+ * materializing a scalar where the template declared "no value" is the
61862
+ * invention this classification exists to avoid.
61863
+ * Saying "would discard an intrinsic" for all three was the defect the
61864
+ * #1787 review round removed; do not re-collapse it.
61865
+ */
61866
+ function classifyOverlayCurrentValue(current) {
61867
+ if (current === void 0) return "absent";
61868
+ if (current === null) return "literal";
61869
+ if (Array.isArray(current)) {
61870
+ if (current.length === 0) return "unrepresentable";
61871
+ return current.every((e) => typeof e === "string" || typeof e === "number" || typeof e === "boolean") ? "literal" : "unrepresentable";
61872
+ }
61873
+ if (typeof current === "object") return "intrinsic";
61874
+ return "literal";
61875
+ }
61876
+ /**
61877
+ * Human-readable shape of an overlay value, for the refusal error and the
61878
+ * rewrite warning. Bounded: the value is user-supplied and lands in an error
61879
+ * message that a failed export persists, so a large list is summarized rather
61880
+ * than serialized whole.
61881
+ */
61882
+ function describeOverlayValueShape(current) {
61883
+ if (Array.isArray(current)) {
61884
+ if (current.length === 0) return "an empty list";
61885
+ const preview = JSON.stringify(current);
61886
+ const shown = preview.length > 120 ? `${preview.slice(0, 120)}…` : preview;
61887
+ return `a ${current.length}-element list (${shown})`;
61888
+ }
61889
+ if (current === null) return "an explicit null";
61890
+ return `a ${typeof current} (${JSON.stringify(current)})`;
61891
+ }
61892
+ /**
61326
61893
  * Build the phase-2 UPDATE template: full synth template with the same
61327
61894
  * conditional overlay applied to every phase-1 import. Phase-1 and
61328
61895
  * phase-2 must stay symmetric — when `overlayResourceIdentifierOnProperties`
@@ -61357,7 +61924,7 @@ function applyImportOverlayForPhase2(template, phase1Imports) {
61357
61924
  const resourcesMap = resources;
61358
61925
  for (const entry of phase1Imports) {
61359
61926
  const r = resourcesMap[entry.logicalId];
61360
- if (r !== void 0) resourcesMap[entry.logicalId] = overlayResourceIdentifierOnProperties(r, entry);
61927
+ if (r !== void 0) resourcesMap[entry.logicalId] = overlayResourceIdentifierOnProperties(r, entry, false);
61361
61928
  }
61362
61929
  return result;
61363
61930
  }
@@ -62029,8 +62596,8 @@ async function runPerStackImportLoop(args) {
62029
62596
  skipImportSupportPreflight: options.skipImportSupportPreflight === true
62030
62597
  });
62031
62598
  if (plan.blocked.length > 0) {
62032
- const lines = plan.blocked.map((b) => ` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
62033
- throw new Error(`Stack '${meta.cdkdStackName}' has ${plan.blocked.length} resource(s) that block migration:\n${lines.join("\n")}`);
62599
+ const lines = groupBlockedReasons(plan.blocked);
62600
+ throw new Error(`Stack '${meta.cdkdStackName}' has ${new Set(plan.blocked.map((b) => b.logicalId)).size} resource(s) that block migration:\n${lines.join("\n")}`);
62034
62601
  }
62035
62602
  perStackPlans.push({
62036
62603
  cdkdName: meta.cdkdStackName,
@@ -62147,7 +62714,7 @@ async function runPerStackImportLoop(args) {
62147
62714
  }
62148
62715
  const rewrittenNestedRows = /* @__PURE__ */ new Map();
62149
62716
  if (plan.nestedStackRows.length > 0) {
62150
- const phase1BTemplate = filterTemplateForImport(plan.template, plan.phase1Imports);
62717
+ const phase1BTemplate = filterTemplateForImport(plan.template, plan.phase1Imports, false);
62151
62718
  injectDeletionPolicyForImport(phase1BTemplate);
62152
62719
  const phase1BResources = [];
62153
62720
  for (const row of plan.nestedStackRows) {
@@ -76618,7 +77185,7 @@ function createMigrateCommand() {
76618
77185
  */
76619
77186
  function buildProgram() {
76620
77187
  const program = new Command();
76621
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.4");
77188
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.6");
76622
77189
  program.hook("preAction", (_thisCommand, actionCommand) => {
76623
77190
  const { profile } = actionCommand.optsWithGlobals();
76624
77191
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;