@go-to-k/cdkd 0.290.24 → 0.290.25

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.
@@ -1,8 +1,8 @@
1
- import { $ as secretBearing, $n as scrubResourceRecord, $r as Synthesizer, $t as s3BucketArn, A as extractDeploymentEventError, Ai as setAwsClients, An as repairMalformedResourcesForReadOnly, Ar as isCrossRegionRedirect, At as isImpossibleEmptyStoredAttribute, B as toCfnInteger, Bi as LocalStartServiceError, Bn as TEMPLATE_SOURCED_RULES, Br as describeDockerExecFailure, Bt as coerceCfnBoolean, C as buildFinalSnapshotIdentifier, Ci as AssemblyReader, Cn as refuseMalformedOutputs, Cr as loadPublishableAssetManifest, Ct as isTerminationProtectionPropagationError, D as refusesFinalSnapshot, Di as AwsClients, Dn as refuseMalformedState, Dr as assertAssetBucketRegion, Dt as cfnRefValueFromPhysicalId, E as isFinalSnapshotError, Ei as resolveBucketRegion, En as refuseMalformedResourcesForDestroy, Er as BOOTSTRAP_MARKER_PREFIX, Et as carriesFetchableDynamicReference, F as isStatefulRecreateTargetSync, Fn as DagBuilder, Fr as buildDenyExternalAccessPolicy, Ft as isSettledInstanceState, G as gray, Gi as ProvisioningError, Gn as identityKeyFor, Gr as partitionSensitiveEnv, Gt as readConfigString, H as formatResourceLine, Hi as MalformedProducerRecordRefusalError, Hn as createSecretMasker, Hr as dockerSpawnEnvWithSensitive, Ht as configBooleanRefusal, I as renderStatefulReason, Ii as DeployCancelledError, In as TemplateParser, Ir as describeAwsFailure, It as WAFv2WebACLProvider, J as yellow, Ji as StackHasActiveImportsError, Jn as maskSecretsInError, Jr as runDockerStreaming, Jt as requireConfigObject, K as green, Ki as ResourceTimeoutError, Kn as isSingleDynamicReferenceToken, Kr as redactDockerArgvValues, Kt as replayWarn, L as WARM_THROUGHPUT_MEMBERS, Li as DynamicReferenceRegionAmbiguousError, Ln as STATE_SOURCED_BASELINE_RULES, Lr as safeStringify, Lt as normalizeAwsTagsToCfn, M as planOrphanAdoption, Mi as CdkdError, Mn as findSilentDropProperties, Mr as readBootstrapMarkerBody, Mt as isUnboundTemplateParameter, N as MULTI_REGION_RECREATE_BLOCKED_TYPES, Nn as describeTypeWithThrottleRetry, Nr as validateAssetBucketName, Nt as parameterTypeMayLoseSecretIdentity, O as unsupportedFinalSnapshotError, Oi as getAwsClients, On as repairMalformedOutputsForReadOnly, Or as ensureAssetStorage, Ot as coerceParameterTypedValue, P as isStatefulRecreateTargetForReplace, Pi as CrossAccountSecretRefusalError, Pn as withRetry, Pr as validateContainerRepoName, Pt as refStateLookupFromResource, Q as isExportAliasCollision, Qn as redactSecretsForState, Qr as getDockerImageBySourceHash, Qt as withSharedDrainBudget, R as coerceWarmThroughput$1, Ri as IntrinsicResolutionRefusalError, Rn as STATE_SOURCED_CROSS_GENERATION_RULES, Rr as buildDockerImage, Rt as resolveExplicitPhysicalId, S as PRE_DELETE_SNAPSHOT_TYPES, Si as derivePartitionAndUrlSuffix, Sn as refuseMalformedNestedChildOutputs, Sr as createAssetRedirectResolver, St as disableInstanceApiTermination, T as createPreDeleteFinalSnapshot, Tn as refuseMalformedResourcePropertiesForOrphan, Tr as AssetModeResolver, Tt as carriesDynamicReference, U as bold, Ui as NestedStackChildDirectDestroyError, Un as dynamicReferenceTokens, Ur as formatDockerLoginError, Ut as configIntegerRefusal, V as toFiniteNumber, Vn as carriesSecretMask, Vr as describeDockerFailure, Vt as coerceCfnInteger, W as cyan, Wi as PartialFailureError, Wn as errorCauseChain, Wr as getDockerCmd, Wt as configStringRefusal, X as collectPublishedOutputNames, Xn as recordMaskOnlyValue, Xr as stripControlChars, Xt as classifyReplaySecretRegion, Y as collectDeclaredOutputNames, Yi as StackTerminationProtectionError, Yn as maskSecretsInText, Yr as escapeRegExp, Yt as requireConfigString, Z as exportAliasCollisionScrubWarning, Zn as recoverMaskedOutput, Zr as AssetManifestLoader, Zt as producerRegionsFromState, _ as isSecretBearingReferenceString, _i as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, _n as malformedRenderedContainersWarning, _r as shouldRetainResource, _t as isWaitAbandonedError, a as DeploymentEventsStore, aa as isTransientServerError, ai as resolveCaptureObservedState, an as applyCallerIdentityCredentials, ar as forceQuitRecoveryClause, at as clearOnUpdateRemoval, b as bindingSkippedOutputs, bi as PARTITION_TABLE, bn as malformedStateRefusalMessage, br as WorkGraph, bt as deleteIndeterminateGuards, c as replayFailedOperations, ca as retryClassificationText, ci as resolveStateBucketWithDefaultAndSource, cn as INTRINSIC_KEYS, cr as CUSTOM_RESOURCE_RESPONSE_PREFIX, ct as createMaskedRetryLogger, d as updatePartialReason, di as warnDeprecatedNoPrefixCliFlag, dn as hasReadableResources, dr as hasReadableExportSet, dt as beginCommandInterruptScope, ea as normalizeAwsError, ei as synthesisStatusMessage, en as s3BucketDomainName, er as LockManager, et as secretBearingStateKeyWarning, f as withResourceDeadline, fi as CFN_TEMPLATE_BODY_LIMIT, fn as malformedExportNamesWarning, fr as importableOutputKeys, ft as endCommandInterruptScope, g as bagHoldsSecretExpression, gi as uploadCfnTemplate, gn as malformedOutputsWarning, gr as orphansCarriedFrom, gt as CloudControlProvider, h as computeImplicitDeleteEdges, hi as findLargeInlineResources, hn as malformedOutputsRefusalMessage, hr as orphansAfterRollback, ht as startInterruptWatch, i as DeploymentEventsReader, ia as isThrottlingError, ii as resolveAutoAssetStorage, in as applyRoleArnIfSet, ir as buildLockContentionMessage, it as collectInlinePolicyNamesManagedBySiblings, j as makeSiblingClaimReader, jn as findActionableSilentDrops, jr as parseBootstrapMarker, jt as isNamelessDynamicReferenceError, k as makeCanonicalizePropertiesFn, ki as resetAwsClients, kn as repairMalformedResourcePropertiesForReadOnly, kr as getBootstrapMarkerKey, kt as getAccountInfo, l as replayRollback, li as resolveUseCdkBootstrapAssets, ln as STATE_RESOURCES_MALFORMED, lr as DEFAULT_STATE_PREFIX, lt as maskDeep, m as IMPLICIT_DELETE_DEPENDENCIES, mi as MIGRATE_TMP_PREFIX, mn as malformedLocalOutputsWarning, mr as isReadableBag, mt as isInterruptedWaitError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, na as isMarkedNonRetryable, ni as getLegacyStateBucketName, nn as s3BucketRegionalDomainName, nr as rebuildClientForBucketRegion, nt as getCurrentResourceSecrets, o as planFailedOps, oa as markNonRetryable, oi as resolveSkipPrefix, on as callerEnvCredentials, or as formatLockExpiry, ot as ProviderRegistry, p as maskingRetryLogger, pi as CFN_TEMPLATE_URL_LIMIT, pn as malformedExportSourceWarning, pr as importableOutputs, pt as interruptWatchListenerCount, q as red, qi as ResourceUpdateNotSupportedError, qn as markSameGenerationBag, qr as runDockerForeground, qt as requireConfigArray, r as DeployEngine, ra as isRetryableTransientError, ri as resolveApp, rn as s3BucketWebsiteUrl, rr as buildForceUnlockCommand, rt as IAMRoleProvider, s as planRollback, sa as markRedactedCause, si as resolveStateBucketWithDefault, sn as DiffCalculator, sr as shellQuote, st as wouldReturnToSdkProvider, t as DEFAULT_RESOURCE_TIMEOUT_MS, ta as withErrorHandling, ti as getDefaultStateBucketName, tn as s3BucketDualStackDomainName, tr as S3StateBackend, tt as secretSafeKeyDisplay, u as updatePartialMessage, ui as stateBucketExistenceConfirmed, un as hasReadableOutputs, ur as exportNamesCarriedFrom, ut as maskerOrIdentity, v as keptWholeReasonText, vi as decodeListingKey, vn as malformedResourcePropertiesWarning, vr as AssetPublisher, vt as slowCcOperationTimeoutMs, w as ccRoutedFinalSnapshotError, wi as processStackMessages, wn as refuseMalformedOutputsForDestroy, wr as rewriteTemplateAssetReferences, wt as IntrinsicFunctionResolver, x as ATOMIC_FINAL_SNAPSHOT_TYPES, xi as canonicalizeRegion, xn as producerRecordKey, xr as buildAssetRedirectMap, xt as deleteSkipReason, y as mergeNoChangeOutputs, yi as expectedOwnerParam, yn as malformedResourcesWarning, yr as stringifyValue, yt as UNSPECIFIED_SKIP_REASON, z as isWarmThroughputDecrease, zi as LocalInvokeBuildError$1, zn as STATE_SOURCED_READBACK_RULES, zr as describeDockerCapturedOutput, zt as assertRegionMatch } from "./deploy-engine-BcNBidfy.js";
1
+ import { $ as secretBearing, $i as normalizeAwsError, $n as redactSecretsForState, $r as synthesisStatusMessage, $t as s3BucketArn, A as extractDeploymentEventError, An as repairMalformedResourcePropertiesForReadOnly, Ar as getBootstrapMarkerKey, At as isImpossibleEmptyStoredAttribute, B as toCfnInteger, Bn as STATE_SOURCED_READBACK_RULES, Br as describeDockerFailure, Bt as coerceCfnBoolean, C as buildFinalSnapshotIdentifier, Ci as processStackMessages, Cn as refuseMalformedNestedChildOutputs, Cr as createAssetRedirectResolver, Ct as isTerminationProtectionPropagationError, D as refusesFinalSnapshot, Di as getAwsClients, Dn as refuseMalformedResourcesForDestroy, Dr as BOOTSTRAP_MARKER_PREFIX, Dt as cfnRefValueFromPhysicalId, E as isFinalSnapshotError, Ei as AwsClients, En as refuseMalformedResourcePropertiesForOrphan, Er as AssetModeResolver, Et as carriesFetchableDynamicReference, F as isStatefulRecreateTargetSync, Fi as DeployCancelledError, Fn as withRetry, Fr as validateContainerRepoName, Ft as isSettledInstanceState, G as gray, Gi as ResourceTimeoutError, Gn as errorCauseChain, Gr as redactDockerArgvValues, Gt as readConfigString, H as formatResourceLine, Hi as NestedStackChildDirectDestroyError, Hn as carriesSecretMask, Hr as formatDockerLoginError, Ht as configBooleanRefusal, I as renderStatefulReason, Ii as DynamicReferenceRegionAmbiguousError, In as DagBuilder, Ir as buildDenyExternalAccessPolicy, It as WAFv2WebACLProvider, J as yellow, Ji as StackTerminationProtectionError, Jn as markSameGenerationBag, Jr as escapeRegExp, Jt as requireConfigObject, K as green, Ki as ResourceUpdateNotSupportedError, Kn as identityKeyFor, Kr as runDockerForeground, Kt as replayWarn, L as WARM_THROUGHPUT_MEMBERS, Li as IntrinsicResolutionRefusalError, Ln as TemplateParser, Lr as buildDockerImage, Lt as normalizeAwsTagsToCfn, M as planOrphanAdoption, Mn as findActionableSilentDrops, Mr as parseBootstrapMarker, Mt as isUnboundTemplateParameter, N as MULTI_REGION_RECREATE_BLOCKED_TYPES, Ni as CrossAccountSecretRefusalError, Nn as findSilentDropProperties, Nr as readBootstrapMarkerBody, Nt as parameterTypeMayLoseSecretIdentity, O as unsupportedFinalSnapshotError, Oi as resetAwsClients, On as refuseMalformedState, Or as assertAssetBucketRegion, Ot as coerceParameterTypedValue, P as isStatefulRecreateTargetForReplace, Pn as describeTypeWithThrottleRetry, Pr as validateAssetBucketName, Pt as refStateLookupFromResource, Q as isExportAliasCollision, Qn as recoverMaskedOutput, Qr as Synthesizer, Qt as withSharedDrainBudget, R as coerceWarmThroughput$1, Ri as LocalInvokeBuildError$1, Rn as STATE_SOURCED_BASELINE_RULES, Rr as describeDockerCapturedOutput, Rt as resolveExplicitPhysicalId, S as PRE_DELETE_SNAPSHOT_TYPES, Si as AssemblyReader, Sn as refuseDivergentRecordRegionForDestroy, Sr as buildAssetRedirectMap, St as disableInstanceApiTermination, T as createPreDeleteFinalSnapshot, Ti as resolveBucketRegion, Tn as refuseMalformedOutputsForDestroy, Tr as rewriteTemplateAssetReferences, Tt as carriesDynamicReference, U as bold, Ui as PartialFailureError, Un as createSecretMasker, Ur as getDockerCmd, Ut as configIntegerRefusal, V as toFiniteNumber, Vi as MalformedProducerRecordRefusalError, Vn as TEMPLATE_SOURCED_RULES, Vr as dockerSpawnEnvWithSensitive, Vt as coerceCfnInteger, W as cyan, Wi as ProvisioningError, Wn as dynamicReferenceTokens, Wr as partitionSensitiveEnv, Wt as configStringRefusal, X as collectPublishedOutputNames, Xn as maskSecretsInText, Xr as AssetManifestLoader, Xt as classifyReplaySecretRegion, Y as collectDeclaredOutputNames, Yn as maskSecretsInError, Yr as stripControlChars, Yt as requireConfigString, Z as exportAliasCollisionScrubWarning, Zn as recordMaskOnlyValue, Zr as getDockerImageBySourceHash, Zt as producerRegionsFromState, _ as isSecretBearingReferenceString, _i as decodeListingKey, _n as malformedRenderedContainersWarning, _r as orphansCarriedFrom, _t as isWaitAbandonedError, a as DeploymentEventsStore, aa as markNonRetryable, ai as resolveSkipPrefix, an as applyCallerIdentityCredentials, ar as buildLockContentionMessage, at as clearOnUpdateRemoval, b as bindingSkippedOutputs, bi as canonicalizeRegion, bn as malformedStateRefusalMessage, br as stringifyValue, bt as deleteIndeterminateGuards, c as replayFailedOperations, ca as describeAwsFailure, ci as resolveUseCdkBootstrapAssets, cn as INTRINSIC_KEYS, cr as shellQuote, ct as createMaskedRetryLogger, d as updatePartialReason, di as CFN_TEMPLATE_BODY_LIMIT, dn as hasReadableResources, dr as exportNamesCarriedFrom, dt as beginCommandInterruptScope, ea as withErrorHandling, ei as getDefaultStateBucketName, en as s3BucketDomainName, er as scrubResourceRecord, et as secretBearingStateKeyWarning, f as withResourceDeadline, fi as CFN_TEMPLATE_URL_LIMIT, fn as malformedExportNamesWarning, fr as hasReadableExportSet, ft as endCommandInterruptScope, g as bagHoldsSecretExpression, gi as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, gn as malformedOutputsWarning, gr as orphansAfterRollback, gt as CloudControlProvider, h as computeImplicitDeleteEdges, hi as uploadCfnTemplate, hn as malformedOutputsRefusalMessage, hr as isReadableBag, ht as startInterruptWatch, i as DeploymentEventsReader, ia as isTransientServerError, ii as resolveCaptureObservedState, in as applyRoleArnIfSet, ir as buildForceUnlockCommand, it as collectInlinePolicyNamesManagedBySiblings, j as makeSiblingClaimReader, ji as CdkdError, jn as repairMalformedResourcesForReadOnly, jr as isCrossRegionRedirect, jt as isNamelessDynamicReferenceError, k as makeCanonicalizePropertiesFn, ki as setAwsClients, kn as repairMalformedOutputsForReadOnly, kr as ensureAssetStorage, kt as getAccountInfo, l as replayRollback, la as safeStringify, li as stateBucketExistenceConfirmed, ln as STATE_RESOURCES_MALFORMED, lr as CUSTOM_RESOURCE_RESPONSE_PREFIX, lt as maskDeep, m as IMPLICIT_DELETE_DEPENDENCIES, mi as findLargeInlineResources, mn as malformedLocalOutputsWarning, mr as importableOutputs, mt as isInterruptedWaitError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, na as isRetryableTransientError, ni as resolveApp, nn as s3BucketRegionalDomainName, nr as S3StateBackend, nt as getCurrentResourceSecrets, o as planFailedOps, oa as markRedactedCause, oi as resolveStateBucketWithDefault, on as callerEnvCredentials, or as forceQuitRecoveryClause, ot as ProviderRegistry, p as maskingRetryLogger, pi as MIGRATE_TMP_PREFIX, pn as malformedExportSourceWarning, pr as importableOutputKeys, pt as interruptWatchListenerCount, q as red, qi as StackHasActiveImportsError, qn as isSingleDynamicReferenceToken, qr as runDockerStreaming, qt as requireConfigArray, r as DeployEngine, ra as isThrottlingError, ri as resolveAutoAssetStorage, rn as s3BucketWebsiteUrl, rr as rebuildClientForBucketRegion, rt as IAMRoleProvider, s as planRollback, sa as retryClassificationText, si as resolveStateBucketWithDefaultAndSource, sn as DiffCalculator, sr as formatLockExpiry, st as wouldReturnToSdkProvider, t as DEFAULT_RESOURCE_TIMEOUT_MS, ta as isMarkedNonRetryable, ti as getLegacyStateBucketName, tn as s3BucketDualStackDomainName, tr as LockManager, tt as secretSafeKeyDisplay, u as updatePartialMessage, ui as warnDeprecatedNoPrefixCliFlag, un as hasReadableOutputs, ur as DEFAULT_STATE_PREFIX, ut as maskerOrIdentity, v as keptWholeReasonText, vi as expectedOwnerParam, vn as malformedResourcePropertiesWarning, vr as shouldRetainResource, vt as slowCcOperationTimeoutMs, w as ccRoutedFinalSnapshotError, wn as refuseMalformedOutputs, wr as loadPublishableAssetManifest, wt as IntrinsicFunctionResolver, x as ATOMIC_FINAL_SNAPSHOT_TYPES, xi as derivePartitionAndUrlSuffix, xn as producerRecordKey, xr as WorkGraph, xt as deleteSkipReason, y as mergeNoChangeOutputs, yi as PARTITION_TABLE, yn as malformedResourcesWarning, yr as AssetPublisher, yt as UNSPECIFIED_SKIP_REASON, z as isWarmThroughputDecrease, zi as LocalStartServiceError, zn as STATE_SOURCED_CROSS_GENERATION_RULES, zr as describeDockerExecFailure, zt as assertRegionMatch } from "./deploy-engine-DtFiFMxh.js";
2
2
  import { C as displaySafe, S as displayIdent, _ as withSkipPrefix, b as STACK_REF_MAX_CODE_POINTS, c as PATTERN_B_NAME_OPTIONS, f as explicitNamePropertyFor, h as getCurrentStackName, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as generateResourceNameWithFallback, n as getLogger, o as runStackBuffered, p as generateResourceName, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES, v as withStackName, w as truncateCodePoints, x as UNRENDERABLE } from "./logger-CkHVgzLx.js";
3
3
  import { n as awsClientDefaults } from "./aws-client-defaults-BoIxyCir.js";
4
- import { t as getCdkdVersion } from "./version-PsB8OjhM.js";
5
- import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-CHeJf5zw.js";
4
+ import { t as getCdkdVersion } from "./version-CduXA6UL.js";
5
+ import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-DL62n7uG.js";
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
7
  import { createHash, randomBytes, randomUUID } from "node:crypto";
8
8
  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, NotFound, 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";
@@ -3132,7 +3132,11 @@ async function probeStatefulRecreateTargetsAsync(targets, clients, logger = getL
3132
3132
  } catch (e) {
3133
3133
  if (e instanceof ResourceNotFoundException$5) {
3134
3134
  let regionVerified = true;
3135
- const recordedRegion = foldRegion(clients.expectedRegion);
3135
+ if (clients.expectedRegionDiverged) {
3136
+ regionVerified = false;
3137
+ logger.warn(`--recreate-via-cc-api / --recreate-via-sdk-provider: CloudWatch Logs reported ${target.logicalId} (log group ${target.physicalId}) missing, but this stack's state record names a region that is not the one its S3 key holds, so cdkd cannot attribute that not-found to any region — run 'cdkd state show' with --verbose to see what the record claims, and repair it. Re-run with --force-stateful-recreation if the log group really is disposable (that flag clears the data guard for every target in the run). Until then it is treated as NOT provably empty.`);
3138
+ }
3139
+ const recordedRegion = clients.expectedRegionDiverged ? void 0 : foldRegion(clients.expectedRegion);
3136
3140
  if (recordedRegion) {
3137
3141
  let clientRegion;
3138
3142
  try {
@@ -17831,6 +17835,20 @@ function hasUsableDeclaredCapacity(entry) {
17831
17835
  */
17832
17836
  const PROVISIONED_CAPACITY_MEMBERS = ["ReadCapacityUnits", "WriteCapacityUnits"];
17833
17837
  /**
17838
+ * The two `OnDemandThroughput` members, named once for the same reason
17839
+ * {@link PROVISIONED_CAPACITY_MEMBERS} is (issue
17840
+ * [#3265](https://github.com/go-to-k/cdkd/issues/3265)). CloudFormation's
17841
+ * `OnDemandThroughput` block carries the same two members at the TABLE level
17842
+ * and inside every `GlobalSecondaryIndexes[]` entry, and all FOUR send sites
17843
+ * read them through
17844
+ * {@link DynamoDBTableProvider.coerceOnDemandCeilingsForSend}.
17845
+ *
17846
+ * Both are `Long` in the SDK model and BOTH ARE OPTIONAL — the property that
17847
+ * makes this block's drop decision differ from its `ProvisionedThroughput`
17848
+ * sibling's. See {@link DynamoDBTableProvider.warnUnusableOnDemandCeiling}.
17849
+ */
17850
+ const ON_DEMAND_CEILING_MEMBERS = ["MaxReadRequestUnits", "MaxWriteRequestUnits"];
17851
+ /**
17834
17852
  * The capacity both TABLE-level forwarders substitute for a member the
17835
17853
  * template does not declare at all — `create()`'s `CreateTable` and the
17836
17854
  * BillingMode flip's `UpdateTable`. It is the pre-issue-#3147 `?? 5`, kept
@@ -18290,14 +18308,20 @@ function reverseMapSecondaryIndex(live, desired, bagInformative) {
18290
18308
  * in PR review round 6; the carve-out is gone because the divergence is.
18291
18309
  * - `ProvisionedThroughput` / `OnDemandThroughput` keep TRUTHINESS, which is
18292
18310
  * still what their write path does: `applyGsiUpdates` gates on
18293
- * `gsi.ProvisionedThroughput` alone, `OnDemandThroughput` is forwarded
18294
- * verbatim on every path, and the `ProvisionedThroughput` coercion issue
18295
- * [#3255](https://github.com/go-to-k/cdkd/issues/3255) added to ALL FOUR
18311
+ * `gsi.ProvisionedThroughput` / `gsi.OnDemandThroughput` alone, and NEITHER
18312
+ * block's Integer coercion — `ProvisionedThroughput`'s from issue
18313
+ * [#3255](https://github.com/go-to-k/cdkd/issues/3255), across ALL FOUR
18296
18314
  * per-index send sites (`create()`'s mapper plus `applyGsiUpdates`'
18297
- * adopted-repair, `Create` and same-name `Update` actions) never
18298
- * SUPPRESSES the block — it rewrites members inside a block that is sent
18299
- * either way, so "would cdkd send this block" is unchanged by it, even
18300
- * where both members were dropped and an empty block goes out. Widening
18315
+ * adopted-repair, `Create` and same-name `Update` actions);
18316
+ * `OnDemandThroughput`'s from issue
18317
+ * [#3265](https://github.com/go-to-k/cdkd/issues/3265), across its own four
18318
+ * (the two TABLE-level forwards plus `create()`'s mapper and
18319
+ * `applyGsiUpdates`' `Create` action) — ever SUPPRESSES the block: each
18320
+ * rewrites members inside a block that is sent either way, so "would cdkd
18321
+ * send this block" is unchanged by them, even where both members were
18322
+ * dropped and an empty block goes out. That is the reason
18323
+ * {@link DynamoDBTableProvider.coerceOnDemandCeilingsForSend} drops a
18324
+ * MEMBER and never the block. Widening
18301
18325
  * `isSendableWarmThroughput` over them would be wrong twice over — it reads
18302
18326
  * members those blocks do not have (`ReadUnitsPerSecond` vs
18303
18327
  * `ReadCapacityUnits`), so every declared capacity would read as undeclared.
@@ -18495,7 +18519,7 @@ var DynamoDBTableProvider = class {
18495
18519
  WriteCapacityUnits: tableCapacityForSend(pt, "WriteCapacityUnits", onUnusable)
18496
18520
  };
18497
18521
  }
18498
- if (properties["OnDemandThroughput"]) createParams.OnDemandThroughput = properties["OnDemandThroughput"];
18522
+ if (properties["OnDemandThroughput"]) createParams.OnDemandThroughput = this.coerceOnDemandCeilingsForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["OnDemandThroughput"], maskSecrets);
18499
18523
  const createWarmThroughput = this.coerceWarmThroughputForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"], maskSecrets);
18500
18524
  if (createWarmThroughput) createParams.WarmThroughput = createWarmThroughput;
18501
18525
  else this.warnRefusedWarmThroughput(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"], maskSecrets);
@@ -18664,7 +18688,7 @@ var DynamoDBTableProvider = class {
18664
18688
  if (properties["OnDemandThroughput"]) {
18665
18689
  await this.dynamoDBClient.send(new UpdateTableCommand({
18666
18690
  TableName: physicalId,
18667
- OnDemandThroughput: properties["OnDemandThroughput"]
18691
+ OnDemandThroughput: this.coerceOnDemandCeilingsForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["OnDemandThroughput"], maskSecrets)
18668
18692
  }));
18669
18693
  await this.waitForTableActiveAfterUpdate(physicalId);
18670
18694
  this.logger.debug(`Updated OnDemandThroughput on DynamoDB table ${physicalId}`);
@@ -19334,7 +19358,7 @@ var DynamoDBTableProvider = class {
19334
19358
  KeySchema: gsi.KeySchema,
19335
19359
  Projection: gsi.Projection,
19336
19360
  ...gsi.ProvisionedThroughput ? { ProvisionedThroughput: this.indexCapacityForSend(this.indexScopeAt(name, physicalId, maskSecrets), gsi.ProvisionedThroughput, maskSecrets) } : {},
19337
- ...gsi.OnDemandThroughput ? { OnDemandThroughput: gsi.OnDemandThroughput } : {},
19361
+ ...gsi.OnDemandThroughput ? { OnDemandThroughput: this.coerceOnDemandCeilingsForSend(this.indexScopeAt(name, physicalId, maskSecrets), gsi.OnDemandThroughput, maskSecrets) } : {},
19338
19362
  ...createWarm ? { WarmThroughput: createWarm } : {}
19339
19363
  } });
19340
19364
  } else {
@@ -19443,16 +19467,20 @@ var DynamoDBTableProvider = class {
19443
19467
  return this.coerceIndexCapacityBlock(scope, declared, maskSecrets) ?? declared;
19444
19468
  }
19445
19469
  /**
19446
- * One `CreateTable` index entry with BOTH of its throughput blocks coerced —
19447
- * `WarmThroughput` (PR review round 6) and `ProvisionedThroughput` (issue
19448
- * [#3255](https://github.com/go-to-k/cdkd/issues/3255)).
19470
+ * One `CreateTable` index entry with ALL THREE of its throughput blocks
19471
+ * coerced — `WarmThroughput` (PR review round 6), `ProvisionedThroughput`
19472
+ * (issue [#3255](https://github.com/go-to-k/cdkd/issues/3255)) and
19473
+ * `OnDemandThroughput` (issue
19474
+ * [#3265](https://github.com/go-to-k/cdkd/issues/3265)).
19449
19475
  *
19450
19476
  * `create()` forwards the declared `GlobalSecondaryIndexes` array to
19451
19477
  * `CreateTable`, so it is a SEND SITE for the same per-index values the
19452
19478
  * update path coerces. `WarmThroughput` was the one the round-5 sweep
19453
19479
  * missed; `ProvisionedThroughput` was missed by round 6 AND by issue #3147's
19454
- * own enumeration, because this forwarder is a CAST rather than a call, so
19455
- * neither sweep's grep could see it. The BillingMode flip's per-index reader
19480
+ * own enumeration; `OnDemandThroughput` was missed a THIRD time, by #3265's
19481
+ * own three-site grep — all for one reason, that this forwarder is a CAST
19482
+ * rather than a call, so no sweep's grep could see it. The
19483
+ * BillingMode flip's per-index reader
19456
19484
  * ({@link readCapacityNumber}) has been on the grammar since #3147, so until
19457
19485
  * now create and flip disagreed about the same property: a CFn-legal
19458
19486
  * `ReadCapacityUnits: '7'` reached `CreateTable` as a STRING in a `number`
@@ -19464,18 +19492,20 @@ var DynamoDBTableProvider = class {
19464
19492
  * and a provider that edits it in place would change what state reports cdkd
19465
19493
  * sent.
19466
19494
  *
19467
- * An entry that is not a plain object, and one declaring NEITHER throughput
19468
- * block, is returned UNCHANGED — identity included. An entry that DOES
19469
- * declare one is rebuilt whenever that block's coercion changes anything;
19470
- * the `WarmThroughput` half rebuilds even for an already-numeric value,
19471
- * which is pre-existing behaviour and costs one spread per declared block.
19495
+ * An entry that is not a plain object, and one declaring NONE of the three
19496
+ * throughput blocks, is returned UNCHANGED — identity included. An entry that
19497
+ * DOES declare one is rebuilt whenever that block's coercion changes
19498
+ * anything; the `WarmThroughput` half rebuilds even for an already-numeric
19499
+ * value, which is pre-existing behaviour and costs one spread per declared
19500
+ * block.
19472
19501
  */
19473
19502
  coerceIndexThroughputForCreate(logicalId, entry, maskSecrets = (text) => text) {
19474
19503
  if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
19475
19504
  const bag = entry;
19476
19505
  const declaredWarm = bag["WarmThroughput"];
19477
19506
  const declaredCapacity = bag["ProvisionedThroughput"];
19478
- if (declaredWarm === void 0 && !isPlainCapacityBlock(declaredCapacity)) return entry;
19507
+ const declaredOnDemand = bag["OnDemandThroughput"];
19508
+ if (declaredWarm === void 0 && !isPlainCapacityBlock(declaredCapacity) && !isPlainCapacityBlock(declaredOnDemand)) return entry;
19479
19509
  const scope = this.indexScope(entry.IndexName, logicalId, maskSecrets);
19480
19510
  let out = entry;
19481
19511
  if (declaredWarm !== void 0) {
@@ -19494,6 +19524,13 @@ var DynamoDBTableProvider = class {
19494
19524
  ...out,
19495
19525
  ProvisionedThroughput: capacity
19496
19526
  };
19527
+ if (isPlainCapacityBlock(declaredOnDemand)) {
19528
+ const onDemand = this.coerceOnDemandCeilingsForSend(scope, declaredOnDemand, maskSecrets);
19529
+ if (onDemand !== declaredOnDemand) out = {
19530
+ ...out,
19531
+ OnDemandThroughput: onDemand
19532
+ };
19533
+ }
19497
19534
  return out;
19498
19535
  }
19499
19536
  /**
@@ -19535,9 +19572,14 @@ var DynamoDBTableProvider = class {
19535
19572
  * the template had declared it, with no later call to correct it. Only
19536
19573
  * the TABLE level defaults.
19537
19574
  * - **A block that is not a plain object is forwarded VERBATIM**, the
19538
- * fail-OPEN direction this file takes everywhere: an unresolved intrinsic
19539
- * or a mis-nested template value is AWS's to reject by name, and that is
19540
- * also the pre-existing behaviour of this forwarder.
19575
+ * fail-OPEN direction this file takes everywhere: a STRING, a NUMBER or an
19576
+ * ARRAY block is AWS's to reject by name, and that is also the pre-existing
19577
+ * behaviour of this forwarder. An unresolved intrinsic (`{Ref: 'X'}`) is
19578
+ * NOT an example of it — that IS a plain object, so it takes the member
19579
+ * loop and survives through the preserve-unknown-member rule below. Same
19580
+ * destination, different route; corrected by the review of issue
19581
+ * [#3265](https://github.com/go-to-k/cdkd/issues/3265), which found the
19582
+ * wrong example here and at {@link coerceOnDemandCeilingsForSend}.
19541
19583
  *
19542
19584
  * A member name the grammar does not know is preserved untouched, for the
19543
19585
  * same fail-open reason — a misspelled `ReadCapacityUnit` reaches AWS and is
@@ -19560,6 +19602,178 @@ var DynamoDBTableProvider = class {
19560
19602
  return rewritten;
19561
19603
  }
19562
19604
  /**
19605
+ * One `OnDemandThroughput` block for the wire, with each DECLARED member read
19606
+ * through CloudFormation's DynamoDB Integer grammar (issue
19607
+ * [#3265](https://github.com/go-to-k/cdkd/issues/3265)).
19608
+ *
19609
+ * Called from ALL FOUR send sites, which is the whole point — the block was
19610
+ * forwarded VERBATIM at every one of them, so a CFn-legal
19611
+ * `MaxReadRequestUnits: '100'` reached AWS as the STRING `"100"` in a `Long`
19612
+ * field and the request failed, while a `' 100 '` / `'0x64'` / `'1e2'`
19613
+ * CloudFormation refuses the template for was forwarded unchanged. The sites:
19614
+ *
19615
+ * 1. `create()`'s TABLE-level `CreateTable.OnDemandThroughput`;
19616
+ * 2. `create()`'s PER-INDEX `CreateTable.GlobalSecondaryIndexes[]` forward,
19617
+ * through {@link coerceIndexThroughputForCreate} — a CAST of the whole
19618
+ * entry array rather than a named `OnDemandThroughput` expression, which
19619
+ * is why issue #3265's own grep enumerated three sites and not four (the
19620
+ * same blind spot that hid this forwarder from issue #3147's sweep and
19621
+ * from #3255's round 6, one property over);
19622
+ * 3. `update()`'s TABLE-level `UpdateTable.OnDemandThroughput`;
19623
+ * 4. `applyGsiUpdates`' per-index `Create` action.
19624
+ *
19625
+ * FOUR and not six: `UpdateGlobalSecondaryIndexAction` declares
19626
+ * `OnDemandThroughput` too, and neither the adopted-index repair nor the
19627
+ * same-name `Update` arm ever sets it — so a ceiling EDIT on a live index is
19628
+ * silently discarded today. That is a MISSING SEND rather than a grammar
19629
+ * defect, so it is not routed here; issue
19630
+ * [#3287](https://github.com/go-to-k/cdkd/issues/3287) holds it, and it will
19631
+ * read through this function when it lands.
19632
+ *
19633
+ * THREE decisions, each with a plausible alternative:
19634
+ *
19635
+ * - **A rejected spelling drops the MEMBER, never the BLOCK.** The same
19636
+ * DECISION {@link coerceIndexCapacityBlock} and {@link tableCapacityForSend}
19637
+ * already take one property over, so this file's capacity and ceiling
19638
+ * families answer identically about one template value; it keeps a sibling
19639
+ * member the template spelled LEGALLY; and it leaves `indexDeclares`'
19640
+ * TRUTHINESS gate ("would cdkd SEND this block") true without
19641
+ * qualification, so the #1767 drift-side predicate needs no new arm. An
19642
+ * all-rejected block therefore goes out EMPTY, as the capacity sibling's
19643
+ * does. ("The same decision", NOT the same SIGNATURE — the capacity
19644
+ * sibling answers `undefined` for "nothing to rewrite" while this one
19645
+ * returns the declared block by IDENTITY. The review of this issue caught
19646
+ * an earlier "byte for byte the shape" here, which was false and is the
19647
+ * kind of claim that invites a caller to swap one for the other.)
19648
+ * - **What a rejected spelling COSTS differs from the capacity sibling's,
19649
+ * and it is stated at {@link warnUnusableOnDemandCeiling} rather than
19650
+ * assumed.** Both `OnDemandThroughput` members are SDK-optional, so
19651
+ * DynamoDB does not necessarily reject a request that omits one — which is
19652
+ * why the announcement cannot reuse the capacity wording, which promises
19653
+ * AWS will name the member. **It is NOT a promise of success either**, and
19654
+ * an earlier revision made it one: the outcome is per PATH (create leaves
19655
+ * no maximum, update KEEPS the live one since only `-1` removes it) and an
19656
+ * ALL-rejected block is documented invalid at every one of the four send
19657
+ * positions ("you must specify `MaxReadRequestUnits`,
19658
+ * `MaxWriteRequestUnits`, or both"), so that request should FAIL. cdkd has
19659
+ * not measured the empty-block send against live DynamoDB; the message
19660
+ * under-claims accordingly.
19661
+ * - **A block that is not a plain object is forwarded VERBATIM**, the
19662
+ * fail-OPEN direction this file takes everywhere: a STRING, a NUMBER or an
19663
+ * ARRAY `OnDemandThroughput` is AWS's to reject by name, and that is the
19664
+ * pre-existing behaviour of every one of these forwarders. Note what this
19665
+ * arm does NOT cover: an unresolved intrinsic (`{Ref: 'X'}`) IS a plain
19666
+ * object, so it takes the member loop instead and survives through the
19667
+ * preserve-unknown-member rule below — same destination, different route.
19668
+ * {@link isPlainCapacityBlock}'s own doc is the authority; the review of
19669
+ * this issue found that example wrong here and at
19670
+ * {@link coerceIndexCapacityBlock}.
19671
+ *
19672
+ * A member name the grammar does not know is preserved untouched, for the
19673
+ * same fail-open reason. Returns the DECLARED object by IDENTITY when nothing
19674
+ * needs rewriting, so a numeric template rebuilds nothing and
19675
+ * {@link coerceIndexThroughputForCreate} can tell "unchanged" from "rewritten"
19676
+ * without a structural compare.
19677
+ *
19678
+ * **Known residual, deliberately not closed here.** Wherever the call
19679
+ * SUCCEEDS, state records the spelling the TEMPLATE declared while AWS holds
19680
+ * a different ceiling (none on a create, the previous one on an update) — the
19681
+ * permanent phantom-drift class
19682
+ * `.claude/rules/provider-property-fidelity.md` covers, which
19683
+ * {@link coerceIndexCapacityBlock} escapes only because ITS drop makes AWS
19684
+ * fail. Closing it needs `effectiveProperties` AND its
19685
+ * `canonicalizeDesiredProperties` twin (the rule requires the pair; shipping
19686
+ * the first alone re-issues a no-op `UpdateTable` on every later deploy), and
19687
+ * this provider implements neither today. Tracked as issue
19688
+ * [#3286](https://github.com/go-to-k/cdkd/issues/3286); the warning names the
19689
+ * residual so a user is not left to discover it from a `cdkd drift` line. The
19690
+ * `AWS::DynamoDB::GlobalTable` sibling answers the SAME way for the same
19691
+ * spelling — `collectTableOnDemandCeilings` coerces through
19692
+ * `coerceCfnInteger` and a rejected ceiling is reported and LEFT UNSENT, with
19693
+ * AWS untouched and no substituted value (its 5/5 FALLBACK is its PROVISIONED
19694
+ * capacity answer, not its ceiling one) — so the two types do not diverge.
19695
+ */
19696
+ coerceOnDemandCeilingsForSend(scope, declared, maskSecrets) {
19697
+ if (!isPlainCapacityBlock(declared)) return declared;
19698
+ const block = declared;
19699
+ let rewritten;
19700
+ for (const member of ON_DEMAND_CEILING_MEMBERS) {
19701
+ if (!(member in block)) continue;
19702
+ const coerced = coerceCfnInteger(block[member]);
19703
+ if (coerced === block[member]) continue;
19704
+ rewritten ??= { ...block };
19705
+ if (coerced === void 0) {
19706
+ this.warnUnusableOnDemandCeiling(scope, member, block[member], maskSecrets);
19707
+ delete rewritten[member];
19708
+ } else rewritten[member] = coerced;
19709
+ }
19710
+ return rewritten ?? block;
19711
+ }
19712
+ /**
19713
+ * Announce an `OnDemandThroughput` member a forwarder DROPPED (issue
19714
+ * [#3265](https://github.com/go-to-k/cdkd/issues/3265)).
19715
+ *
19716
+ * A SEPARATE sentence from {@link warnUnusableProvisionedCapacity}, and the
19717
+ * difference is the load-bearing part rather than a wording preference: that
19718
+ * one can promise "DynamoDB will reject the request naming this member",
19719
+ * because both `ProvisionedThroughput` members are REQUIRED wherever the
19720
+ * block is declared. Both `OnDemandThroughput` members are OPTIONAL, so the
19721
+ * same promise would send the user looking for a failure that may never come.
19722
+ *
19723
+ * What it says instead is PER PATH, because the outcome genuinely differs and
19724
+ * an earlier revision of this message asserted ONE outcome for all of them —
19725
+ * wrongly in both directions (the review of this issue):
19726
+ *
19727
+ * - on a CREATE the table comes up with no maximum for the dropped half;
19728
+ * - on an UPDATE DynamoDB KEEPS whatever maximum the table already carries.
19729
+ * Omitting a member is NOT "no maximum" there — `-1` is AWS's documented
19730
+ * way to REMOVE one (`OnDemandThroughput`'s own SDK model doc), so a
19731
+ * dropped member silently leaves the live ceiling in force;
19732
+ * - when NO member survived, the block {@link coerceOnDemandCeilingsForSend}
19733
+ * sends is EMPTY. AWS's model says at all four of this provider's send
19734
+ * positions that "if you use this parameter, you must specify
19735
+ * `MaxReadRequestUnits`, `MaxWriteRequestUnits`, or both", which reads
19736
+ * like a LOUD failure — and it is NOT enforced.
19737
+ *
19738
+ * MEASURED us-east-1 2026-09-17, on the go-to-k/cdkd#3291 review's
19739
+ * request: `CreateTable` with `OnDemandThroughput: {}` and
19740
+ * `BillingMode: PAY_PER_REQUEST` is ACCEPTED, the table reaches ACTIVE,
19741
+ * and `DescribeTable` reports NO `OnDemandThroughput` at all. So the
19742
+ * all-rejected case is the SILENT one, not the loud one: the deploy goes
19743
+ * green with no ceiling applied.
19744
+ *
19745
+ * That matters because it is a REGRESSION IN LOUDNESS introduced by this
19746
+ * very change. Before it, a `' 100 '` reached AWS as a string in a Long
19747
+ * field and the request FAILED, certainly. After it, the value is dropped
19748
+ * and the deploy succeeds. The warning is therefore the ONLY signal a user
19749
+ * gets for the all-rejected case, which is why it is emitted per member
19750
+ * and why its text was corrected to say so rather than to promise a
19751
+ * failure that does not come.
19752
+ *
19753
+ * The caller cannot tell the three cases apart from inside the member loop,
19754
+ * so the message names all three rather than asserting the one it cannot
19755
+ * know.
19756
+ *
19757
+ * And it names the residual: where the request does succeed, state keeps the
19758
+ * spelling the template declared and `cdkd drift` reports it until the
19759
+ * template is corrected. `-1` is called out because a user who meant the
19760
+ * removal must not read "spell it as a decimal integer" as excluding it.
19761
+ *
19762
+ * The CloudFormation clause is HEDGED ("is expected to"), and deliberately.
19763
+ * The live A/B that settled this grammar was run on
19764
+ * `ProvisionedThroughput.ReadCapacityUnits`, not on `MaxReadRequestUnits`.
19765
+ * Both are Integer members of the same type validated at the same properties
19766
+ * layer, so the extrapolation is sound and is what issue #3265 reasons from --
19767
+ * but it IS an extrapolation, and a user-facing string asserting it flatly
19768
+ * about an unmeasured property would be the kind of claim nothing downstream
19769
+ * re-checks. Measure it and the hedge can go. Raised by the
19770
+ * go-to-k/cdkd#3291 spec review.
19771
+ */
19772
+ warnUnusableOnDemandCeiling(scope, member, raw, maskSecrets) {
19773
+ const warn = (message) => this.logger.warn(maskSecrets(message));
19774
+ warn(`${scope}: OnDemandThroughput.${member} ${JSON.stringify(maskLeafValue$1(raw, maskSecrets))} is not an Integer CloudFormation accepts (an optional sign and decimal digits only — no surrounding whitespace, hex, exponent or decimal point), so it was NOT sent and NO ceiling was substituted for it. Both OnDemandThroughput members are OPTIONAL to DynamoDB, so — unlike a ProvisionedThroughput member — AWS does not necessarily reject the request, and what happens next depends on the path: a CREATE gets no maximum for this half; an UPDATE KEEPS whatever maximum the table already carries, since only an explicit -1 removes one; and if NEITHER member was usable the block is sent EMPTY, which AWS ACCEPTS (measured us-east-1 2026-09-17) and stores as no maximum at all, so the deploy SUCCEEDS with no ceiling applied. cdkd's state keeps the value the template declared and \`cdkd drift\` reports the difference until the template is corrected; CloudFormation is expected to refuse the same template at properties validation. Check for an unresolved intrinsic, or spell the value as a decimal integer (\`-1\` removes an existing maximum).`);
19775
+ }
19776
+ /**
19563
19777
  * Announce a `ProvisionedThroughput` member a forwarder DROPPED (issue
19564
19778
  * [#3147](https://github.com/go-to-k/cdkd/issues/3147); widened to the
19565
19779
  * per-index create forwarder by issue
@@ -62206,6 +62420,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62206
62420
  const guardIndeterminateTargets = /* @__PURE__ */ new Set();
62207
62421
  const regionForState = state.region ?? ctx.baseRegion;
62208
62422
  refuseMalformedResourcesForDestroy(state, stackName, regionForState);
62423
+ refuseDivergentRecordRegionForDestroy(state, stackName, regionForState, ctx.divergentBodyRegion);
62209
62424
  const resourceCount = Object.keys(state.resources).length;
62210
62425
  const orphanCount = (state.orphans ?? []).length;
62211
62426
  refuseMalformedOutputsForDestroy(state, stackName, regionForState);
@@ -62265,7 +62480,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62265
62480
  try {
62266
62481
  await ctx.lockManager.releaseLock(stackName, regionForState);
62267
62482
  } catch (releaseErr) {
62268
- logger.warn(`Failed to release lock after empty-state cleanup: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
62483
+ logger.warn(`Failed to release lock after empty-state cleanup: ${describeAwsFailure(releaseErr).detail}`);
62269
62484
  } finally {
62270
62485
  process.removeListener("SIGINT", emptySigintHandler);
62271
62486
  }
@@ -62324,7 +62539,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62324
62539
  try {
62325
62540
  switchedClients?.destroy();
62326
62541
  } catch (destroyError) {
62327
- logger.debug(`Failed to destroy region-scoped AWS clients: ${destroyError instanceof Error ? destroyError.message : String(destroyError)}`);
62542
+ logger.debug(`Failed to destroy region-scoped AWS clients: ${describeAwsFailure(destroyError).detail}`);
62328
62543
  }
62329
62544
  };
62330
62545
  if (stackRegion && stackRegion !== ctx.baseRegion) {
@@ -62393,7 +62608,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62393
62608
  try {
62394
62609
  await ctx.lockManager.releaseLock(stackName, regionForState);
62395
62610
  } catch (releaseErr) {
62396
- logger.warn(`Failed to release lock after strong-ref refusal/failure: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
62611
+ logger.warn(`Failed to release lock after strong-ref refusal/failure: ${describeAwsFailure(releaseErr).detail}`);
62397
62612
  }
62398
62613
  restoreBaseRegionAndClients();
62399
62614
  process.removeListener("SIGINT", sigintHandler);
@@ -62433,7 +62648,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62433
62648
  await ctx.stateBackend.saveState(stackName, regionForState, buildDestroySnapshot());
62434
62649
  logger.debug(`State persisted after deleting ${logicalId}`);
62435
62650
  } catch (error) {
62436
- logger.warn(`Failed to persist state after deleting ${logicalId} (continuing): ${error instanceof Error ? error.message : String(error)}`);
62651
+ logger.warn(`Failed to persist state after deleting ${logicalId} (continuing): ${describeAwsFailure(error).detail}`);
62437
62652
  }
62438
62653
  });
62439
62654
  };
@@ -62629,7 +62844,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62629
62844
  persistStateAfterDelete(logicalId);
62630
62845
  } catch (error) {
62631
62846
  renderer.removeTask(logicalId);
62632
- const msg = error instanceof Error ? error.message : String(error);
62847
+ const msg = describeAwsFailure(error).detail;
62633
62848
  if (!isInterruptedWaitError(error) && !isFinalSnapshotError(error) && !isMarkedNonRetryable(error) && !isWaitAbandonedError(error) && (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException"))) {
62634
62849
  logger.debug(` ${logicalId} already deleted, removing from state`);
62635
62850
  result.deletedCount++;
@@ -62661,7 +62876,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62661
62876
  error: extractDeploymentEventError(wrapped)
62662
62877
  });
62663
62878
  } else {
62664
- logger.error(` ✗ Failed to delete ${logicalId}:`, String(error));
62879
+ logger.error(` ✗ Failed to delete ${logicalId}:`, safeStringify(error));
62665
62880
  result.errorCount++;
62666
62881
  failedStateTargets.add(stateTargetFor(logicalId, resource.resourceType));
62667
62882
  ctx.eventRecorder?.record({
@@ -62693,7 +62908,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62693
62908
  try {
62694
62909
  await ctx.stateBackend.saveState(stackName, regionForState, buildDestroySnapshot());
62695
62910
  } catch (error) {
62696
- logger.warn(`Failed to persist remaining state after partial destroy: ${error instanceof Error ? error.message : String(error)}. The state file may still list already-deleted resources; a re-run resolves them idempotently.`);
62911
+ logger.warn(`Failed to persist remaining state after partial destroy: ${describeAwsFailure(error).detail}. The state file may still list already-deleted resources; a re-run resolves them idempotently.`);
62697
62912
  }
62698
62913
  if (result.interrupted) logger.warn(`Destroy interrupted — ${Object.keys(remainingResources).length} resource(s) not deleted. State preserved.`);
62699
62914
  else if (result.errorCount > 0) logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
@@ -62724,14 +62939,14 @@ async function runDestroyForStack(stackName, state, ctx) {
62724
62939
  try {
62725
62940
  renderer.stop();
62726
62941
  } catch (rendererError) {
62727
- logger.debug(`Live renderer teardown failed (continuing to release the lock): ${rendererError instanceof Error ? rendererError.message : String(rendererError)}`);
62942
+ logger.debug(`Live renderer teardown failed (continuing to release the lock): ${describeAwsFailure(rendererError).detail}`);
62728
62943
  }
62729
62944
  await saveChain;
62730
62945
  logger.debug("Releasing lock...");
62731
62946
  try {
62732
62947
  await ctx.lockManager.releaseLock(stackName, regionForState);
62733
62948
  } catch (releaseErr) {
62734
- logger.warn(`Failed to release lock for stack '${stackName}': ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
62949
+ logger.warn(`Failed to release lock for stack '${stackName}': ${describeAwsFailure(releaseErr).detail}`);
62735
62950
  }
62736
62951
  } finally {
62737
62952
  process.removeListener("SIGINT", sigintHandler);
@@ -63004,6 +63219,7 @@ var NestedStackProvider = class {
63004
63219
  providerRegistry: ctx.providerRegistry,
63005
63220
  baseAwsClients: ctx.awsClients,
63006
63221
  baseRegion: childRegion,
63222
+ ...childStateData.divergentBodyRegion !== void 0 && { divergentBodyRegion: childStateData.divergentBodyRegion },
63007
63223
  stateBucket: ctx.stateBucket,
63008
63224
  skipConfirmation: true,
63009
63225
  ...ctx.exportIndexStore && { exportIndexStore: ctx.exportIndexStore },
@@ -64378,7 +64594,8 @@ async function deployCommand(stacks, options) {
64378
64594
  clients: {
64379
64595
  s3: stackAwsClients.s3,
64380
64596
  cloudWatchLogs: stackAwsClients.cloudWatchLogs,
64381
- expectedRegion: stateForRecreateCheck?.state.region
64597
+ expectedRegion: stateForRecreateCheck?.state.region,
64598
+ ...stateForRecreateCheck?.divergentBodyRegion !== void 0 && { expectedRegionDiverged: true }
64382
64599
  },
64383
64600
  forceStatefulRecreation: options.forceStatefulRecreation ?? false
64384
64601
  });
@@ -74363,6 +74580,7 @@ async function destroyCommand(stackArgs, options) {
74363
74580
  providerRegistry,
74364
74581
  baseAwsClients: awsClients,
74365
74582
  baseRegion: region,
74583
+ ...stateResult.divergentBodyRegion !== void 0 && { divergentBodyRegion: stateResult.divergentBodyRegion },
74366
74584
  ...options.profile && { profile: options.profile },
74367
74585
  stateBucket,
74368
74586
  statePrefix: options.statePrefix,
@@ -76117,10 +76335,10 @@ async function exportCommand(stackArg, options) {
76117
76335
  }
76118
76336
  const cfnStackName = options.cfnStackName ?? resolvedStackName;
76119
76337
  const targetRegion = await pickStackRegion$1(stateBackend, resolvedStackName, synthedRegion, options.stackRegion);
76120
- logger.info(`Migrating cdkd stack '${resolvedStackName}' (${targetRegion}) → CloudFormation stack '${cfnStackName}'`);
76338
+ logger.info(`Migrating cdkd stack '${resolvedStackName}' (${safeSegment(targetRegion)}) → CloudFormation stack '${cfnStackName}'`);
76121
76339
  await assertCfnStackAbsent(awsClients.cloudFormation, cfnStackName);
76122
76340
  const stateData = await stateBackend.getState(resolvedStackName, targetRegion);
76123
- if (!stateData) throw new Error(`No cdkd state found for stack '${resolvedStackName}' (${targetRegion}). Nothing to migrate.`);
76341
+ if (!stateData) throw new Error(`No cdkd state found for stack '${resolvedStackName}' (${safeSegment(targetRegion)}). Nothing to migrate.`);
76124
76342
  const { state, etag, migrationPending } = stateData;
76125
76343
  const exportJournal = await stateBackend.loadRollbackJournal(resolvedStackName, targetRegion);
76126
76344
  if (exportJournal && exportJournal.segments.length > 0) {
@@ -76237,7 +76455,7 @@ async function exportCommand(stackArg, options) {
76237
76455
  const phase2Note = phase2Creates.length > 0 ? ` Phase 2 will then CREATE ${phase2Creates.length} non-importable resource(s) (invoking each Custom Resource's onCreate handler).` : "";
76238
76456
  const recreateNote = recreateBeforePhase2.length > 0 ? ` cdkd will also DELETE ${recreateBeforePhase2.length} AWS resource(s) between phases so CFn can re-CREATE them in phase 2 (brief unavailability window — see plan above for the affected resources).` : "";
76239
76457
  const unchangedClaim = recreateBeforePhase2.length > 0 ? ` All other AWS resources are unchanged on import.` : ` AWS resources are unchanged on import.`;
76240
- if (!await confirmPrompt$3(`Create CloudFormation stack '${cfnStackName}' by importing ${phase1Imports.length} resource(s) from cdkd state '${resolvedStackName}' (${targetRegion})?` + phase2Note + recreateNote + unchangedClaim + ` cdkd state for '${resolvedStackName}' will be deleted on success.`)) {
76458
+ if (!await confirmPrompt$3(`Create CloudFormation stack '${cfnStackName}' by importing ${phase1Imports.length} resource(s) from cdkd state '${resolvedStackName}' (${safeSegment(targetRegion)})?` + phase2Note + recreateNote + unchangedClaim + ` cdkd state for '${resolvedStackName}' will be deleted on success.`)) {
76241
76459
  logger.info("Migration cancelled. cdkd state and CloudFormation are unchanged.");
76242
76460
  return;
76243
76461
  }
@@ -76272,7 +76490,7 @@ async function exportCommand(stackArg, options) {
76272
76490
  logger.info(` ✓ deleted ${entry.physicalId}`);
76273
76491
  } catch (err) {
76274
76492
  const msg = err instanceof Error ? err.message : String(err);
76275
- throw new Error(`Phase 1 (IMPORT) succeeded; pre-delete of ${entry.logicalId} (${entry.resourceType}, physicalId: ${entry.physicalId}) failed: ${msg}\n\nThe CloudFormation stack '${cfnStackName}' contains the phase-1 imports but the IMPORT-unsupported resources (${recreateBeforePhase2.length} total) still exist in AWS unmanaged. cdkd state is UNCHANGED. Re-running \`cdkd export\` does NOT work — the existing-stack check rejects it. To recover manually:\n 1. Fix the failure cause (typically IAM permissions for the underlying AWS API — e.g. apigatewayv2:DeleteStage for AWS::ApiGatewayV2::Stage).\n 2. Delete the remaining AWS-side IMPORT-unsupported resources by hand:\n aws apigatewayv2 delete-stage --api-id <ApiId> --stage-name <StageName>\n (one per entry in the pre-delete list logged above).\n 3. Run the phase-2 UPDATE manually with the full synth template:\n aws cloudformation create-change-set --stack-name ${cfnStackName} \\\n --change-set-name cdkd-phase2-retry --change-set-type UPDATE \\\n --template-body file://<full-template.json>\n 4. Once phase 2 succeeds, run: cdkd state orphan ${resolvedStackName}\n to clean up cdkd's stale state record.`);
76493
+ throw new Error(`Phase 1 (IMPORT) succeeded; pre-delete of ${entry.logicalId} (${entry.resourceType}, physicalId: ${entry.physicalId}) failed: ${msg}\n\nThe CloudFormation stack '${cfnStackName}' contains the phase-1 imports but the IMPORT-unsupported resources (${recreateBeforePhase2.length} total) still exist in AWS unmanaged. cdkd state is UNCHANGED. Re-running \`cdkd export\` does NOT work — the existing-stack check rejects it. To recover manually:\n 1. Fix the failure cause (typically IAM permissions for the underlying AWS API — e.g. apigatewayv2:DeleteStage for AWS::ApiGatewayV2::Stage).\n 2. Delete the remaining AWS-side IMPORT-unsupported resources by hand:\n aws apigatewayv2 delete-stage --api-id <ApiId> --stage-name <StageName>\n (one per entry in the pre-delete list logged above).\n 3. Run the phase-2 UPDATE manually with the full synth template:\n aws cloudformation create-change-set --stack-name ${cfnStackName} \\\n --change-set-name cdkd-phase2-retry --change-set-type UPDATE \\\n --template-body file://<full-template.json>\n 4. Once phase 2 succeeds, run: ${orphanCommandFor(resolvedStackName, targetRegion)}\n to clean up cdkd's stale state record.`);
76276
76494
  }
76277
76495
  }
76278
76496
  const phase2Count = phase2Creates.length + recreateBeforePhase2.length;
@@ -76291,10 +76509,10 @@ async function exportCommand(stackArg, options) {
76291
76509
  const recreateNote = recreateBeforePhase2.length > 0 ? ` - cdkd deleted ${recreateBeforePhase2.length} AWS resource(s) before phase 2 (Stage etc.). They are gone in AWS but absent from the CFn stack. Running phase 2 UPDATE manually will CFn-CREATE them fresh.\n` : "";
76292
76510
  throw new Error(`Phase 1 (IMPORT) succeeded; phase 2 (UPDATE) failed: ${msg}\n\nThe CloudFormation stack '${cfnStackName}' now contains the imported resources but is missing ${phase2Count} resource(s) (${phase2Creates.length} Custom Resource(s) + ${recreateBeforePhase2.length} IMPORT-unsupported). cdkd state is UNCHANGED so you can inspect what's in it, but DO NOT run \`cdkd deploy\` against this stack (the imported resources are now CFn-managed). To recover:\n` + recreateNote + ` 1. Fix the failure cause (typically an onCreate Lambda error).
76293
76511
  2. Re-run the phase 2 UPDATE manually with the full synth template:
76294
- aws cloudformation create-change-set --stack-name ${cfnStackName} \\\n --change-set-name cdkd-phase2-retry --change-set-type UPDATE \\\n --template-body file://<full-template.json>\n 3. Once phase 2 succeeds, run: cdkd state orphan ${resolvedStackName}\n to clean up cdkd's stale state record.`);
76512
+ aws cloudformation create-change-set --stack-name ${cfnStackName} \\\n --change-set-name cdkd-phase2-retry --change-set-type UPDATE \\\n --template-body file://<full-template.json>\n 3. Once phase 2 succeeds, run: ${orphanCommandFor(resolvedStackName, targetRegion)}\n to clean up cdkd's stale state record.`);
76295
76513
  }
76296
76514
  await stateBackend.deleteState(resolvedStackName, targetRegion);
76297
- logger.info(`cdkd state for '${resolvedStackName}' (${targetRegion}) removed. Manage the stack with 'cdk deploy' or 'aws cloudformation' from here on.`);
76515
+ logger.info(`cdkd state for '${resolvedStackName}' (${safeSegment(targetRegion)}) removed. Manage the stack with 'cdk deploy' or 'aws cloudformation' from here on.`);
76298
76516
  printNextSteps({
76299
76517
  cfnStackName,
76300
76518
  cdkStackName: resolvedStackName,
@@ -76322,7 +76540,7 @@ async function pickStackRegion$1(stateBackend, stackName, synthRegion, flag) {
76322
76540
  }
76323
76541
  if (flag) {
76324
76542
  if (!refs.find((r) => r.region === flag)) {
76325
- const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
76543
+ const seen = refs.map((r) => safeSegment(r.region ?? "(legacy)")).join(", ");
76326
76544
  throw new Error(`No state found for stack '${stackName}' in region '${flag}'. Available regions: ${seen}.`);
76327
76545
  }
76328
76546
  return flag;
@@ -76331,7 +76549,7 @@ async function pickStackRegion$1(stateBackend, stackName, synthRegion, flag) {
76331
76549
  if (refs.find((r) => r.region === synthRegion)) return synthRegion;
76332
76550
  }
76333
76551
  if (refs.length === 1) return refs[0].region ?? synthRegion ?? "";
76334
- const regions = refs.map((r) => r.region ?? "(legacy)").join(", ");
76552
+ const regions = refs.map((r) => safeSegment(r.region ?? "(legacy)")).join(", ");
76335
76553
  throw new Error(`Stack '${stackName}' has state in multiple regions: ${regions}. Re-run with --stack-region <region> to disambiguate.`);
76336
76554
  }
76337
76555
  function parseTemplateFile(path) {
@@ -76395,9 +76613,81 @@ function isPhase2CreatableType(resourceType) {
76395
76613
  * Module-scoped rather than local to the walker: the first cut of this change
76396
76614
  * guarded the walker's two refusals and left the root one, which is the shape
76397
76615
  * the issue is about.
76616
+ *
76617
+ * The parameter is `unknown`, not `string | undefined`, because a record-body
76618
+ * value is only TYPED as a string: the region-mismatch refusal below has
76619
+ * rendered `{"toString": null}` since issue #2947, and `getState`'s
76620
+ * `divergentBodyRegion` (#3328) is declared honestly as `unknown`.
76621
+ * `displaySafe` has always accepted `unknown` and coerces totally, so this
76622
+ * widens the signature to what the function already did.
76623
+ *
76624
+ * CAPPED as well as sanitized, since go-to-k/cdkd#3328 review round 2. Every
76625
+ * value reaching here is a state-record field or an S3 key segment, so a
76626
+ * planted multi-kilobyte one — or an ARRAY, which `String` comma-joins with no
76627
+ * bound — pushes the rest of the refusal off the reader's screen. The bound is
76628
+ * the STATE-RECORD grammar rather than `IDENT_MAX_CODE_POINTS` because a
76629
+ * legitimate multi-level nested-child stack name runs past 255 code points,
76630
+ * and this function renders those; `truncateCodePoints` so a cut never lands
76631
+ * inside a surrogate pair.
76398
76632
  */
76399
76633
  function safeSegment(value) {
76400
- return displaySafe(value, { asciiOnly: true }) || "<unrenderable>";
76634
+ const safe = displaySafe(value, { asciiOnly: true });
76635
+ if (!safe) return UNRENDERABLE;
76636
+ const { text, truncated } = truncateCodePoints(safe, STACK_REF_MAX_CODE_POINTS);
76637
+ return truncated ? `${text} [cut: ${safe.length - text.length} more characters withheld]` : text;
76638
+ }
76639
+ /**
76640
+ * The free-form counterpart of {@link safeSegment}: an AWS error message, not
76641
+ * an identifier.
76642
+ *
76643
+ * **Its BEHAVIOUR is identical today, and the separation is about the CALLER's
76644
+ * contract rather than about the transformation** — stated plainly because an
76645
+ * earlier revision of this comment claimed a "tighter cap" for identifiers that
76646
+ * does not exist, and a false distinction is worse than none. What differs is
76647
+ * what a caller may DO with the result: `safeSegment`'s callers compare it
76648
+ * against the raw value to decide whether an identity renders EXACTLY
76649
+ * ({@link orphanCommandFor}), which is meaningless for a sentence; and
76650
+ * `display-safe.ts` says its identifier helpers are not for free-form text, so
76651
+ * routing a message through the one named `Segment` invites the next reader to
76652
+ * give it the identifier grammar's cap. Both sanitize with the ASCII allowlist
76653
+ * and bound the length, because both are untrusted text bound for a terminal.
76654
+ */
76655
+ function safeDetail(value) {
76656
+ const safe = displaySafe(value instanceof Error ? value.message : value, { asciiOnly: true });
76657
+ if (!safe) return UNRENDERABLE;
76658
+ const { text, truncated } = truncateCodePoints(safe, STACK_REF_MAX_CODE_POINTS);
76659
+ return truncated ? `${text} [cut: ${safe.length - text.length} more characters withheld]` : text;
76660
+ }
76661
+ /**
76662
+ * A `cdkd state orphan` line for a record-derived stack name — PASTEABLE only
76663
+ * when that name renders EXACTLY, and a template otherwise (issue
76664
+ * [#3328](https://github.com/go-to-k/cdkd/issues/3328), review round 3).
76665
+ *
76666
+ * Three messages in this file suggested that command with a name minted from
76667
+ * `Object.keys(state.resources)` (`walkCdkdStateStackTree` builds
76668
+ * `` `${parent}~${logicalId}` ``), i.e. body content anyone able to write one
76669
+ * state key chooses. Two rendered it RAW, so a newline or a CSI sequence in a
76670
+ * logical id forged whole lines around a DELETE command; the third sanitized
76671
+ * but did not shell-quote, so `'`, `;`, `|` and `` ` `` survived into a line an
76672
+ * operator pastes into a shell.
76673
+ *
76674
+ * Both halves of `malformed-resources-bag.ts`'s rule, and for its reasons:
76675
+ * sanitize THEN shell-quote, and gate the substituted form on the identity
76676
+ * rendering exactly — `displaySafe` TRIMS, so a name ending in a space renders
76677
+ * byte-identically to a healthy sibling, and `state orphan` DELETES a record.
76678
+ * Every caller emits the result LAST and unwrapped so the quoting composes; that
76679
+ * is an instruction to the CALLER and must not leak into the message it builds.
76680
+ *
76681
+ * `region` is REQUIRED even though the flag is optional on the command: omitting
76682
+ * `--stack-region` makes `state orphan` drop the record for that name in EVERY
76683
+ * region, which is wider than any of these messages describes. A helper whose
76684
+ * job is to make a destructive suggestion safe should not have an arm that
76685
+ * widens one.
76686
+ */
76687
+ function orphanCommandFor(stackName, region) {
76688
+ const shownStack = safeSegment(stackName);
76689
+ if (!(typeof stackName === "string" && shownStack === stackName && typeof region === "string" && safeSegment(region) === region)) return "cdkd state orphan <stack> --stack-region <region> — spelled out because this record's name or region does NOT render exactly, so another record may render identically; list them as stored with 'cdkd state list --long' and act on the one whose key matches";
76690
+ return `cdkd state orphan ${shellQuote(shownStack)} --stack-region ${shellQuote(safeSegment(region))}`;
76401
76691
  }
76402
76692
  /**
76403
76693
  * Recursively load the cdkd-state tree rooted at `(rootStackName, region)`.
@@ -76452,8 +76742,8 @@ async function walkCdkdStateStackTree(stackName, region, state, stateBackend) {
76452
76742
  if (entry?.resourceType !== "AWS::CloudFormation::Stack") continue;
76453
76743
  const childStackName = `${stackName}~${logicalId}`;
76454
76744
  const childResult = await stateBackend.getState(childStackName, region);
76455
- if (!childResult) throw new Error(`cdkd state is missing nested-child '${safeSegment(childStackName)}' (${safeSegment(region)}). Parent stack '${safeSegment(stackName)}' lists '${safeSegment(logicalId)}' as an ${NESTED_STACK_RESOURCE_TYPE} row but no child state file exists at 'cdkd/${safeSegment(childStackName)}/${safeSegment(region)}/state.json'. The cdkd state tree is inconsistent — re-deploy the parent stack to refresh, or run 'cdkd state orphan ${safeSegment(stackName)}' and re-import.`);
76456
- if (childResult.state.region !== void 0 && childResult.state.region !== region) throw new Error(`cdkd state region mismatch: nested-child '${safeSegment(childStackName)}' has state.region='${safeSegment(childResult.state.region)}' but its parent '${safeSegment(stackName)}' is being walked against region='${safeSegment(region)}'. AWS does not support cross-region nested stacks; the state tree appears corrupt — re-deploy the parent stack to refresh.`);
76745
+ if (!childResult) throw new Error(`cdkd state is missing nested-child '${safeSegment(childStackName)}' (${safeSegment(region)}). Parent stack '${safeSegment(stackName)}' lists '${safeSegment(logicalId)}' as an ${NESTED_STACK_RESOURCE_TYPE} row but no child state file exists at 'cdkd/${safeSegment(childStackName)}/${safeSegment(region)}/state.json'. The cdkd state tree is inconsistent — re-deploy the parent stack to refresh, or drop the parent's record and re-import with: ${orphanCommandFor(stackName, region)}`);
76746
+ if (childResult.divergentBodyRegion !== void 0) throw new Error(`cdkd state region mismatch: nested-child '${safeSegment(childStackName)}' has state.region='${safeSegment(childResult.divergentBodyRegion)}' but its parent '${safeSegment(stackName)}' is being walked against region='${safeSegment(region)}'. AWS does not support cross-region nested stacks; the state tree appears corrupt — re-deploy the parent stack to refresh.`);
76457
76747
  nestedChildren.set(logicalId, await walkCdkdStateStackTree(childStackName, region, childResult.state, stateBackend));
76458
76748
  }
76459
76749
  return {
@@ -76534,7 +76824,7 @@ function flattenCdkdStateTreeRootFirst(tree) {
76534
76824
  */
76535
76825
  function cdkd2cfnStackName(cdkdStackName) {
76536
76826
  const cfnName = cdkdStackName.replace(/~/g, "-");
76537
- if (!/^[a-zA-Z][-a-zA-Z0-9]*$/.test(cfnName)) throw new Error(`cdkd stack name '${cdkdStackName}' maps to CFn stack name '${cfnName}', which violates the CloudFormation stack-name constraint [a-zA-Z][-a-zA-Z0-9]* (must start with a letter; no '_', '.', '/', or other special characters). Supply an explicit name via --cfn-stack-name (root) or --cfn-child-stack-name '<cdkdName>=<cfnName>' (nested child).`);
76827
+ if (!/^[a-zA-Z][-a-zA-Z0-9]*$/.test(cfnName)) throw new Error(`cdkd stack name '${safeSegment(cdkdStackName)}' maps to CFn stack name '${safeSegment(cfnName)}', which violates the CloudFormation stack-name constraint [a-zA-Z][-a-zA-Z0-9]* (must start with a letter; no '_', '.', '/', or other special characters). Supply an explicit name via --cfn-stack-name (root) or --cfn-child-stack-name '<cdkdName>=<cfnName>' (nested child).`);
76538
76828
  return cfnName;
76539
76829
  }
76540
76830
  /**
@@ -76560,7 +76850,7 @@ function parseCfnChildStackNameOverrides(values) {
76560
76850
  if (!cdkdName) throw new Error(`--cfn-child-stack-name '${raw}' has an empty cdkd stack name.`);
76561
76851
  if (!cfnName) throw new Error(`--cfn-child-stack-name '${raw}' has an empty CFn stack name.`);
76562
76852
  if (!/^[a-zA-Z][-a-zA-Z0-9]*$/.test(cfnName)) throw new Error(`--cfn-child-stack-name '${raw}': CFn stack name '${cfnName}' must match [a-zA-Z][-a-zA-Z0-9]* (no '~', no '/', no '_', no '.').`);
76563
- if (out.has(cdkdName)) throw new Error(`--cfn-child-stack-name: duplicate override for cdkd stack '${cdkdName}'. Pass it once with the final CFn name.`);
76853
+ if (out.has(cdkdName)) throw new Error(`--cfn-child-stack-name: duplicate override for cdkd stack '${safeSegment(cdkdName)}'. Pass it once with the final CFn name.`);
76564
76854
  out.set(cdkdName, cfnName);
76565
76855
  }
76566
76856
  return out;
@@ -76646,7 +76936,7 @@ function indexNestedTemplatePaths(template, templateDir) {
76646
76936
  * orchestrator.
76647
76937
  */
76648
76938
  function buildPerStackImportNodes(rootStackName, rootTemplate, rootNestedTemplatePaths, rootTemplateFormat, tree) {
76649
- if (tree.stackName !== rootStackName) throw new Error(`buildPerStackImportNodes: tree root '${tree.stackName}' does not match expected root stack name '${rootStackName}'.`);
76939
+ if (tree.stackName !== rootStackName) throw new Error(`buildPerStackImportNodes: tree root '${safeSegment(tree.stackName)}' does not match expected root stack name '${safeSegment(rootStackName)}'.`);
76650
76940
  const out = /* @__PURE__ */ new Map();
76651
76941
  walk(tree, rootTemplate, rootNestedTemplatePaths, rootTemplateFormat);
76652
76942
  return out;
@@ -76660,7 +76950,7 @@ function buildPerStackImportNodes(rootStackName, rootTemplate, rootNestedTemplat
76660
76950
  });
76661
76951
  for (const [childLogicalId, childNode] of node.nestedChildren) {
76662
76952
  const childTemplatePath = nodeNestedTemplatePaths[childLogicalId];
76663
- if (!childTemplatePath) throw new Error(`cdkd export: nested-stack child '${childLogicalId}' under parent '${node.stackName}' has cdkd state but no Metadata['aws:asset:path'] in the parent template's '${childLogicalId}' row. The synth output and the cdkd state tree are out of sync — re-deploy the parent stack to refresh, or remove the cdkd state for the orphaned child via 'cdkd state orphan ${childNode.stackName}'.`);
76953
+ if (!childTemplatePath) throw new Error(`cdkd export: nested-stack child '${safeSegment(childLogicalId)}' under parent '${safeSegment(node.stackName)}' has cdkd state but no Metadata['aws:asset:path'] in the parent template's '${safeSegment(childLogicalId)}' row. The synth output and the cdkd state tree are out of sync — re-deploy the parent stack to refresh, or remove the cdkd state for the orphaned child with ${orphanCommandFor(childNode.stackName, childNode.region)}`);
76664
76954
  const { template: childTemplate, format: childFormat } = readNestedChildTemplateFile(childTemplatePath, childLogicalId);
76665
76955
  walk(childNode, childTemplate, indexNestedTemplatePaths(childTemplate, path.dirname(childTemplatePath)), childFormat);
76666
76956
  }
@@ -76712,7 +77002,7 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
76712
77002
  blocked.push({
76713
77003
  logicalId,
76714
77004
  resourceType,
76715
- reason: `template has AWS::CloudFormation::Stack row '${logicalId}' but cdkd state has no matching nested-stack entry on parent '${parentStackName}'. Re-deploy or re-import the parent stack to refresh state.`
77005
+ reason: `template has AWS::CloudFormation::Stack row '${logicalId}' but cdkd state has no matching nested-stack entry on parent '${safeSegment(parentStackName)}'. Re-deploy or re-import the parent stack to refresh state.`
76716
77006
  });
76717
77007
  continue;
76718
77008
  }
@@ -77405,13 +77695,13 @@ function reportDriftBaselineGaps(state, logger) {
77405
77695
  const missing = entries.filter(([, r]) => r.observedProperties === void 0);
77406
77696
  if (missing.length === 0) return;
77407
77697
  if (state.version !== void 0 && state.version < 3) {
77408
- logger.warn(`cdkd state schema is v${state.version} (pre-observedProperties). cdkd drift cannot reliably compare against AWS for this stack; the next \`cdk deploy\` after migration may surface spurious changes if AWS has drifted from the template. Run \`cdkd state refresh-observed ${state.stackName}\` (or any redeploy) before export to capture an AWS-current baseline.`);
77698
+ logger.warn(`cdkd state schema is v${state.version} (pre-observedProperties). cdkd drift cannot reliably compare against AWS for this stack; the next \`cdk deploy\` after migration may surface spurious changes if AWS has drifted from the template. Run \`cdkd state refresh-observed ${safeSegment(state.stackName)}\` (or any redeploy) before export to capture an AWS-current baseline.`);
77409
77699
  return;
77410
77700
  }
77411
77701
  const refused = missing.filter(([, r]) => r.observedBaselineRefused === true);
77412
77702
  const refreshable = missing.filter(([, r]) => r.observedBaselineRefused !== true);
77413
77703
  if (refreshable.length > 0) {
77414
- logger.warn(`${refreshable.length} of ${entries.length} resource(s) in cdkd state lack an AWS-current baseline (observedProperties). cdkd drift may produce false positives for them; the next \`cdk deploy\` after migration may surface unexpected changes. Run \`cdkd state refresh-observed ${state.stackName}\` to capture a baseline before export, then \`cdkd drift\` to verify the stack matches AWS.`);
77704
+ logger.warn(`${refreshable.length} of ${entries.length} resource(s) in cdkd state lack an AWS-current baseline (observedProperties). cdkd drift may produce false positives for them; the next \`cdk deploy\` after migration may surface unexpected changes. Run \`cdkd state refresh-observed ${safeSegment(state.stackName)}\` to capture a baseline before export, then \`cdkd drift\` to verify the stack matches AWS.`);
77415
77705
  for (const [logicalId] of refreshable.slice(0, 10)) logger.warn(` ${logicalId}`);
77416
77706
  if (refreshable.length > 10) logger.warn(` ... and ${refreshable.length - 10} more`);
77417
77707
  }
@@ -77954,12 +78244,12 @@ async function buildResolvedParametersPerStack(args) {
77954
78244
  for (const ref of flattenCdkdStateTreeRootFirst(args.tree)) {
77955
78245
  if (ref.stackName === args.rootStackName) continue;
77956
78246
  const node = nodeByName.get(ref.stackName);
77957
- if (!node) throw new Error(`buildResolvedParametersPerStack: tree node '${ref.stackName}' has no per-stack template/state entry. This is a cdkd bug.`);
78247
+ if (!node) throw new Error(`buildResolvedParametersPerStack: tree node '${safeSegment(ref.stackName)}' has no per-stack template/state entry. This is a cdkd bug.`);
77958
78248
  const parentLogicalId = node.state.parentLogicalId;
77959
78249
  const parentStackName = node.state.parentStack;
77960
- if (!parentLogicalId || !parentStackName) throw new Error(`buildResolvedParametersPerStack: child '${node.cdkdName}' state is missing parentLogicalId / parentStack (v6 fields). Re-deploy or re-import the parent stack.`);
78250
+ if (!parentLogicalId || !parentStackName) throw new Error(`buildResolvedParametersPerStack: child '${safeSegment(node.cdkdName)}' state is missing parentLogicalId / parentStack (v6 fields). Re-deploy or re-import the parent stack.`);
77961
78251
  const parentNode = nodeByName.get(parentStackName);
77962
- if (!parentNode) throw new Error(`buildResolvedParametersPerStack: child '${node.cdkdName}' references parent '${parentStackName}' which is not in the per-stack node list — tree shape is inconsistent.`);
78252
+ if (!parentNode) throw new Error(`buildResolvedParametersPerStack: child '${safeSegment(node.cdkdName)}' references parent '${safeSegment(parentStackName)}' which is not in the per-stack node list — tree shape is inconsistent.`);
77963
78253
  const parentParamValues = {};
77964
78254
  for (const p of paramsByCdkdName.get(parentStackName) ?? []) if (p.ParameterKey !== void 0) parentParamValues[p.ParameterKey] = p.ParameterValue;
77965
78255
  const parentResolverContext = {
@@ -78044,7 +78334,7 @@ async function runPerStackImportLoop(args) {
78044
78334
  };
78045
78335
  const nodesByCdkdName = buildPerStackImportNodes(rootStackName, rootTemplate, rootStackInfoNestedTemplates, rootTemplateFormat, tree);
78046
78336
  const leafFirst = flattenCdkdStateTreeLeafFirst(tree);
78047
- for (const n of leafFirst) if (!nodesByCdkdName.has(n.stackName)) throw new Error(`runPerStackImportLoop: missing per-stack template for '${n.stackName}' — tree node has cdkd state but no synth template was loaded. This is a cdkd bug.`);
78337
+ for (const n of leafFirst) if (!nodesByCdkdName.has(n.stackName)) throw new Error(`runPerStackImportLoop: missing per-stack template for '${safeSegment(n.stackName)}' — tree node has cdkd state but no synth template was loaded. This is a cdkd bug.`);
78048
78338
  const perStackPlans = [];
78049
78339
  for (const node of leafFirst) {
78050
78340
  const meta = nodesByCdkdName.get(node.stackName);
@@ -78055,7 +78345,7 @@ async function runPerStackImportLoop(args) {
78055
78345
  });
78056
78346
  if (plan.blocked.length > 0) {
78057
78347
  const lines = groupBlockedReasons(plan.blocked);
78058
- 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")}`);
78348
+ throw new Error(`Stack '${safeSegment(meta.cdkdStackName)}' has ${new Set(plan.blocked.map((b) => b.logicalId)).size} resource(s) that block migration:\n${lines.join("\n")}`);
78059
78349
  }
78060
78350
  perStackPlans.push({
78061
78351
  cdkdName: meta.cdkdStackName,
@@ -78074,7 +78364,7 @@ async function runPerStackImportLoop(args) {
78074
78364
  const totalRecreate = perStackPlans.reduce((acc, p) => acc + p.recreateBeforePhase2.length, 0);
78075
78365
  logger.info("");
78076
78366
  logger.info(`Migrating cdkd nested-stack tree rooted at '${rootStackName}' → CloudFormation (${perStackPlans.length} stack(s), leaf-first):`);
78077
- for (const plan of perStackPlans) logger.info(` [${plan.cdkdName}] → CFn stack '${plan.cfnName}': ${plan.phase1Imports.length} leaf import(s)` + (plan.nestedStackRows.length > 0 ? `, ${plan.nestedStackRows.length} nested-child adoption(s)` : "") + (plan.phase2Creates.length > 0 ? `, ${plan.phase2Creates.length} phase-2 CREATE(s)` : "") + (plan.recreateBeforePhase2.length > 0 ? `, ${plan.recreateBeforePhase2.length} pre-delete + re-CREATE` : ""));
78367
+ for (const plan of perStackPlans) logger.info(` [${safeSegment(plan.cdkdName)}] → CFn stack '${safeSegment(plan.cfnName)}': ${plan.phase1Imports.length} leaf import(s)` + (plan.nestedStackRows.length > 0 ? `, ${plan.nestedStackRows.length} nested-child adoption(s)` : "") + (plan.phase2Creates.length > 0 ? `, ${plan.phase2Creates.length} phase-2 CREATE(s)` : "") + (plan.recreateBeforePhase2.length > 0 ? `, ${plan.recreateBeforePhase2.length} pre-delete + re-CREATE` : ""));
78078
78368
  logger.info("");
78079
78369
  if (totalPhase2Creates > 0 && !options.includeNonImportable) {
78080
78370
  if (options.dryRun) {
@@ -78098,7 +78388,7 @@ async function runPerStackImportLoop(args) {
78098
78388
  if (!options.yes) {
78099
78389
  const phase2Note = totalPhase2Creates > 0 ? ` Phase 2 will CREATE ${totalPhase2Creates} non-importable resource(s) across the tree.` : "";
78100
78390
  const recreateNote = totalRecreate > 0 ? ` cdkd will also pre-DELETE + re-CREATE ${totalRecreate} IMPORT-unsupported resource(s) (brief unavailability window per resource).` : "";
78101
- if (!await confirmPrompt$3(`Create ${perStackPlans.length} CloudFormation stack(s) by importing the cdkd nested-stack tree rooted at '${rootStackName}' (${rootRegion}) — leaf-first, per-stack IMPORT loop (#464 design §4.3).` + phase2Note + recreateNote + " AWS resources are unchanged. cdkd state for every adopted stack will be deleted on success.")) {
78391
+ if (!await confirmPrompt$3(`Create ${perStackPlans.length} CloudFormation stack(s) by importing the cdkd nested-stack tree rooted at '${rootStackName}' (${safeSegment(rootRegion)}) — leaf-first, per-stack IMPORT loop (#464 design §4.3).` + phase2Note + recreateNote + " AWS resources are unchanged. cdkd state for every adopted stack will be deleted on success.")) {
78102
78392
  logger.info("Migration cancelled. cdkd state and CloudFormation are unchanged.");
78103
78393
  return {
78104
78394
  outcome: "cancelled",
@@ -78144,11 +78434,11 @@ async function runPerStackImportLoop(args) {
78144
78434
  resolver: paramResolver,
78145
78435
  stateBackend: deps.stateBackend
78146
78436
  }));
78147
- for (const [cdkdName, skipped] of sessionIntrinsicSkipped) logger.warn(` Child '${cdkdName}': could not resolve intrinsic-valued Parameter(s) ${skipped.join(", ")} at IMPORT time. The child template's Parameter Default values must cover these — otherwise CFn will reject the IMPORT.`);
78437
+ for (const [cdkdName, skipped] of sessionIntrinsicSkipped) logger.warn(` Child '${safeSegment(cdkdName)}': could not resolve intrinsic-valued Parameter(s) ${skipped.join(", ")} at IMPORT time. The child template's Parameter Default values must cover these — otherwise CFn will reject the IMPORT.`);
78148
78438
  try {
78149
78439
  for (let i = 0; i < perStackPlans.length; i++) {
78150
78440
  const plan = perStackPlans[i];
78151
- logger.info(`[${i + 1}/${perStackPlans.length}] Importing cdkd stack '${plan.cdkdName}' → CFn stack '${plan.cfnName}' (${plan.phase1Imports.length} leaf, ${plan.nestedStackRows.length} nested-child adoption)`);
78441
+ logger.info(`[${i + 1}/${perStackPlans.length}] Importing cdkd stack '${safeSegment(plan.cdkdName)}' → CFn stack '${plan.cfnName}' (${plan.phase1Imports.length} leaf, ${plan.nestedStackRows.length} nested-child adoption)`);
78152
78442
  const stackParameters = paramsByCdkdName.get(plan.cdkdName) ?? [];
78153
78443
  const phase1ATemplate = filterTemplateForImport(plan.template, plan.phase1Imports);
78154
78444
  const phase1AResources = plan.phase1Imports.map((entry) => ({
@@ -78157,13 +78447,13 @@ async function runPerStackImportLoop(args) {
78157
78447
  ResourceIdentifier: entry.resourceIdentifier
78158
78448
  }));
78159
78449
  const injectedDeletion = injectDeletionPolicyForImport(phase1ATemplate);
78160
- if (injectedDeletion > 0) logger.info(` Injected DeletionPolicy: Delete on ${injectedDeletion} resource(s) in '${plan.cdkdName}' (required by CFn IMPORT; matches CDK/CFn default).`);
78450
+ if (injectedDeletion > 0) logger.info(` Injected DeletionPolicy: Delete on ${injectedDeletion} resource(s) in '${safeSegment(plan.cdkdName)}' (required by CFn IMPORT; matches CDK/CFn default).`);
78161
78451
  try {
78162
78452
  await submitImportChangeSet(deps.cfnClient, plan.cfnName, phase1ATemplate, phase1AResources, stackParameters, plan.templateFormat, deps.uploadOpts);
78163
78453
  } catch (err) {
78164
- const importedSummary = importedStacks.length > 0 ? importedStacks.map((s) => `${s.cdkdStackName} → ${s.cfnStackName}`).join(", ") : "(none)";
78165
- const remainingSummary = perStackPlans.slice(i).map((p) => p.cdkdName).join(", ");
78166
- throw new Error(`Phase 1A IMPORT changeset failed for cdkd stack '${plan.cdkdName}' (CFn name '${plan.cfnName}'). Stacks imported successfully so far: ${importedSummary}. Stacks not yet imported (cdkd state preserved): ${remainingSummary}. After resolving the underlying cause, re-run 'cdkd export ${rootStackName}' — already-imported children will be adopted as nested references on retry. Cause: ${err instanceof Error ? err.message : String(err)}`, { cause: err instanceof Error ? err : void 0 });
78454
+ const importedSummary = importedStacks.length > 0 ? importedStacks.map((s) => `${safeSegment(s.cdkdStackName)} → ${safeSegment(s.cfnStackName)}`).join(", ") : "(none)";
78455
+ const remainingSummary = perStackPlans.slice(i).map((p) => safeSegment(p.cdkdName)).join(", ");
78456
+ throw new Error(`Phase 1A IMPORT changeset failed for cdkd stack '${safeSegment(plan.cdkdName)}' (CFn name '${plan.cfnName}'). Stacks imported successfully so far: ${importedSummary}. Stacks not yet imported (cdkd state preserved): ${remainingSummary}. After resolving the underlying cause, re-run 'cdkd export ${rootStackName}' — already-imported children will be adopted as nested references on retry. Cause: ${err instanceof Error ? err.message : String(err)}`, { cause: err instanceof Error ? err : void 0 });
78167
78457
  }
78168
78458
  const cfnArn = (await deps.cfnClient.send(new DescribeStacksCommand({ StackName: plan.cfnName }))).Stacks?.[0]?.StackId;
78169
78459
  if (!cfnArn) throw new Error(`runPerStackImportLoop: DescribeStacks returned no StackId for '${plan.cfnName}' immediately after Phase 1A IMPORT — AWS may be in an unexpected state.`);
@@ -78185,7 +78475,7 @@ async function runPerStackImportLoop(args) {
78185
78475
  const phase1BResources = [];
78186
78476
  for (const row of plan.nestedStackRows) {
78187
78477
  const childArn = cfnArnByCdkdName.get(row.childStackName);
78188
- if (!childArn) throw new Error(`runPerStackImportLoop: nested-stack child '${row.childStackName}' has no recorded CFn ARN when processing parent '${plan.cdkdName}'. Leaf-first iteration order should have IMPORTed the child first — this is a cdkd bug.`);
78478
+ if (!childArn) throw new Error(`runPerStackImportLoop: nested-stack child '${safeSegment(row.childStackName)}' has no recorded CFn ARN when processing parent '${safeSegment(plan.cdkdName)}'. Leaf-first iteration order should have IMPORTed the child first — this is a cdkd bug.`);
78189
78479
  const childCfnName = cfnStackNameOf(row.childStackName);
78190
78480
  const childTemplateBody = await fetchCfnStackTemplate(deps.cfnClient, childCfnName);
78191
78481
  const uploaded = await uploadCfnTemplate({
@@ -78198,7 +78488,7 @@ async function runPerStackImportLoop(args) {
78198
78488
  uploadCleanups.push(uploaded.cleanup);
78199
78489
  const childActualTags = (await deps.cfnClient.send(new DescribeStacksCommand({ StackName: childCfnName }))).Stacks?.[0]?.Tags ?? [];
78200
78490
  const originalRow = getResourceFromTemplate(plan.template, row.logicalId);
78201
- if (!originalRow) throw new Error(`runPerStackImportLoop: parent template for '${plan.cdkdName}' has no resource '${row.logicalId}'. State and template are out of sync.`);
78491
+ if (!originalRow) throw new Error(`runPerStackImportLoop: parent template for '${safeSegment(plan.cdkdName)}' has no resource '${row.logicalId}'. State and template are out of sync.`);
78202
78492
  const rewrittenRow = injectRetainAndRewriteTemplateUrl(originalRow, uploaded.url, childActualTags);
78203
78493
  phase1BTemplate["Resources"][row.logicalId] = rewrittenRow;
78204
78494
  rewrittenNestedRows.set(row.logicalId, rewrittenRow);
@@ -78211,8 +78501,8 @@ async function runPerStackImportLoop(args) {
78211
78501
  try {
78212
78502
  await submitImportChangeSet(deps.cfnClient, plan.cfnName, phase1BTemplate, phase1BResources, stackParameters, plan.templateFormat, deps.uploadOpts);
78213
78503
  } catch (err) {
78214
- const importedSummary = importedStacks.length > 0 ? importedStacks.map((s) => `${s.cdkdStackName} → ${s.cfnStackName}`).join(", ") : "(none)";
78215
- throw new Error(`Phase 1B (nested-child adoption) IMPORT changeset failed for parent '${plan.cdkdName}' (CFn name '${plan.cfnName}'). The parent CFn stack exists with its ${plan.phase1Imports.length} leaf resource(s); ${plan.nestedStackRows.length} nested-child adoption(s) did NOT complete. Stacks IMPORTed so far (each is a standalone CFn stack): ${importedSummary}. cdkd state for every stack in the tree is preserved. To recover: (1) fix the underlying cause (typically a template-match validation error per AWS-docs "Nested stack import validation"); (2) clear cdkd state for the migrated stacks via 'cdkd state orphan <stack>' (do NOT 'cdkd destroy' — that would tear down the live AWS resources); (3) re-attempt the parent-side nested adoption manually via the AWS console or CLI per the AWS docs procedure. Cause: ${err instanceof Error ? err.message : String(err)}`, { cause: err instanceof Error ? err : void 0 });
78504
+ const importedSummary = importedStacks.length > 0 ? importedStacks.map((s) => `${safeSegment(s.cdkdStackName)} → ${safeSegment(s.cfnStackName)}`).join(", ") : "(none)";
78505
+ throw new Error(`Phase 1B (nested-child adoption) IMPORT changeset failed for parent '${safeSegment(plan.cdkdName)}' (CFn name '${safeSegment(plan.cfnName)}'). The parent CFn stack exists with its ${plan.phase1Imports.length} leaf resource(s); ${plan.nestedStackRows.length} nested-child adoption(s) did NOT complete. Stacks IMPORTed so far (each is a standalone CFn stack): ${importedSummary}. cdkd state for every stack in the tree is preserved. To recover: (1) fix the underlying cause (typically a template-match validation error per AWS-docs "Nested stack import validation"); (2) clear cdkd state for the migrated stacks via 'cdkd state orphan <stack>' (do NOT 'cdkd destroy' — that would tear down the live AWS resources); (3) re-attempt the parent-side nested adoption manually via the AWS console or CLI per the AWS docs procedure. Cause: ${err instanceof Error ? err.message : String(err)}`, { cause: err instanceof Error ? err : void 0 });
78216
78506
  }
78217
78507
  logger.info(` ✓ Phase 1B: parent '${plan.cfnName}' adopted ${plan.nestedStackRows.length} nested child(ren) via UPDATE-IMPORT.`);
78218
78508
  if (plan.cdkdName !== rootStackName) {
@@ -78222,7 +78512,7 @@ async function runPerStackImportLoop(args) {
78222
78512
  }
78223
78513
  if (plan.recreateBeforePhase2.length > 0) for (const entry of plan.recreateBeforePhase2) {
78224
78514
  const handler = PRE_DELETE_HANDLERS[entry.resourceType];
78225
- if (!handler) throw new Error(`No pre-delete handler registered for ${entry.resourceType} (${entry.logicalId}) in stack '${plan.cdkdName}'. This is a cdkd bug — the resource is in IMPORT_UNSUPPORTED_RECREATABLE_TYPES but lacks a PRE_DELETE_HANDLERS entry.`);
78515
+ if (!handler) throw new Error(`No pre-delete handler registered for ${entry.resourceType} (${entry.logicalId}) in stack '${safeSegment(plan.cdkdName)}'. This is a cdkd bug — the resource is in IMPORT_UNSUPPORTED_RECREATABLE_TYPES but lacks a PRE_DELETE_HANDLERS entry.`);
78226
78516
  logger.info(` Pre-deleting AWS resource for ${entry.logicalId} (${entry.resourceType}) so CFn can re-CREATE in phase 2...`);
78227
78517
  await handler(entry);
78228
78518
  logger.info(` ✓ deleted ${entry.physicalId}`);
@@ -78241,21 +78531,21 @@ async function runPerStackImportLoop(args) {
78241
78531
  const stateDeletionFailures = [];
78242
78532
  for (const node of leafFirst) try {
78243
78533
  await deps.stateBackend.deleteState(node.stackName, node.region);
78244
- logger.info(`cdkd state for '${node.stackName}' (${node.region}) removed.`);
78534
+ logger.info(`cdkd state for '${safeSegment(node.stackName)}' (${safeSegment(node.region)}) removed.`);
78245
78535
  } catch (err) {
78246
78536
  stateDeletionFailures.push({
78247
78537
  stackName: node.stackName,
78248
78538
  region: node.region,
78249
78539
  reason: err instanceof Error ? err.message : String(err)
78250
78540
  });
78251
- logger.warn(`Failed to delete cdkd state for '${node.stackName}' (${node.region}): ${err instanceof Error ? err.message : String(err)}. The stack IS CFn-managed; clean up with 'cdkd state orphan ${node.stackName}'.`);
78541
+ logger.warn(`Failed to delete cdkd state for '${safeSegment(node.stackName)}' (${safeSegment(node.region)}): ${safeDetail(err)}. The stack IS CFn-managed; clean up with: ${orphanCommandFor(node.stackName, node.region)}`);
78252
78542
  }
78253
78543
  if (stateDeletionFailures.length > 0) {
78254
- const lines = stateDeletionFailures.map((f) => ` - cdkd/${f.stackName}/${f.region}/state.json: ${f.reason}`).join("\n");
78255
- throw new Error(`${stateDeletionFailures.length} cdkd state record(s) could not be deleted after a successful per-stack IMPORT loop. Every stack in the tree IS CFn-managed (the migration succeeded AWS-side); orphan state remains under:\n${lines}\nRecover with 'cdkd state orphan <stack>' per record.`);
78544
+ const lines = stateDeletionFailures.map((f) => ` - cdkd/${safeSegment(f.stackName)}/${safeSegment(f.region)}/state.json: ${safeDetail(f.reason)}`).join("\n");
78545
+ throw new Error(`${stateDeletionFailures.length} cdkd state record(s) could not be deleted after a successful per-stack IMPORT loop. Every stack in the tree IS CFn-managed (the migration succeeded AWS-side); orphan state remains under:\n${lines}\nRecover with 'cdkd state orphan <stack> --stack-region <region>' per record.`);
78256
78546
  }
78257
78547
  if (sessionIntrinsicSkipped.size > 0) {
78258
- const detail = [...sessionIntrinsicSkipped.entries()].map(([cdkdName, params]) => `${cdkdName} (${params.join(", ")})`).join("; ");
78548
+ const detail = [...sessionIntrinsicSkipped.entries()].map(([cdkdName, params]) => `${safeSegment(cdkdName)} (${params.join(", ")})`).join("; ");
78259
78549
  logger.warn(`${sessionIntrinsicSkipped.size} stack(s) had intrinsic-valued Parameter(s) that cdkd could not resolve at IMPORT time: ${detail}. Verify each child template's Parameter Default values cover these before the first 'cdk deploy'.`);
78260
78550
  }
78261
78551
  return {
@@ -78269,7 +78559,7 @@ async function runPerStackImportLoop(args) {
78269
78559
  for (let i = acquiredChildLocks.length - 1; i >= 0; i--) {
78270
78560
  const lock = acquiredChildLocks[i];
78271
78561
  await deps.lockManager.releaseLock(lock.stackName, lock.region).catch((err) => {
78272
- logger.warn(`Failed to release lock for '${lock.stackName}' (${lock.region}): ${err instanceof Error ? err.message : String(err)}`);
78562
+ logger.warn(`Failed to release lock for '${safeSegment(lock.stackName)}' (${safeSegment(lock.region)}): ${safeDetail(err)}`);
78273
78563
  });
78274
78564
  }
78275
78565
  }
@@ -79679,9 +79969,29 @@ function createStateOrphanCommand() {
79679
79969
  * state record. AWS resources are left intact.
79680
79970
  *
79681
79971
  * Region scoping: when a stack name has multiple state records spread across
79682
- * regions (PR 1 territory), `--region` selects one. With the current single-
79683
- * region-per-name layout the flag still works — it sets the AWS clients'
79684
- * region and refuses to proceed if the loaded state.region disagrees.
79972
+ * regions (PR 1 territory), `--stack-region` selects one — it picks WHICH
79973
+ * record is loaded, because the region is part of the S3 key.
79974
+ *
79975
+ * What it does with a record whose BODY names a different region than that key
79976
+ * is two things, neither of which this comment described until issue
79977
+ * [#3328](https://github.com/go-to-k/cdkd/issues/3328) — it claimed a refusal
79978
+ * on `state.region` that had never existed anywhere in the command, while the
79979
+ * measured behaviour was the opposite: a planted record at
79980
+ * `.../us-east-1/state.json` carrying `"region": "eu-west-1"` locked, saved
79981
+ * and deleted against `eu-west-1` and reported `✓ State deleted` while the
79982
+ * us-east-1 key survived.
79983
+ *
79984
+ * 1. `S3StateBackend.getState` normalizes the loaded record's `region` to the
79985
+ * KEY's and WARNS, so this command — which hands `stateResult.state`
79986
+ * straight to `runDestroyForStack` — acts on the region it was pointed at.
79987
+ * The read itself refuses NOTHING; `adoptKeyRegion` carries why, and it is
79988
+ * what keeps `state show` / `state list` / `state orphan` working on such a
79989
+ * record.
79990
+ * 2. The DESTROY refuses it when the record still lists resources
79991
+ * (`refuseDivergentRecordRegionForDestroy`, reached through
79992
+ * `runDestroyForStack`), because which region those resources are in is
79993
+ * exactly what the divergence makes undecidable. A resource-less record is
79994
+ * not refused, so cleaning one up still works.
79685
79995
  */
79686
79996
  async function stateDestroyCommand(stackArgs, options) {
79687
79997
  const logger = getLogger();
@@ -79828,6 +80138,7 @@ async function stateDestroyCommand(stackArgs, options) {
79828
80138
  providerRegistry,
79829
80139
  baseAwsClients: setup.awsClients,
79830
80140
  baseRegion: setup.region,
80141
+ ...stateResult.divergentBodyRegion !== void 0 && { divergentBodyRegion: stateResult.divergentBodyRegion },
79831
80142
  ...options.profile && { profile: options.profile },
79832
80143
  stateBucket: setup.bucket,
79833
80144
  statePrefix: options.statePrefix,
@@ -94395,4 +94706,4 @@ function buildProgram() {
94395
94706
 
94396
94707
  //#endregion
94397
94708
  export { buildProgram };
94398
- //# sourceMappingURL=program-DaoJ7P4H.js.map
94709
+ //# sourceMappingURL=program-babtsurS.js.map