@go-to-k/cdkd 0.290.23 → 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-CBzq85r6.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-DxUVmvM5.js";
5
- import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-BOYln2Tw.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 {
@@ -12974,6 +12978,88 @@ function mapDeliveryStatusLogging(attrs, stateCaseMap = /* @__PURE__ */ new Map(
12974
12978
  return result;
12975
12979
  }
12976
12980
 
12981
+ //#endregion
12982
+ //#region src/provisioning/attribute-map.ts
12983
+ /**
12984
+ * The ONE spelling of the attribute-map rule an SDK provider's `create()` /
12985
+ * `update()` / `import()` result follows (issue #3077, closing the `?? ''`
12986
+ * shape `docs/provider-development.md` forbids under "Never store an
12987
+ * empty-string placeholder"):
12988
+ *
12989
+ * an attribute cdkd could not read back is ABSENT from the map — never
12990
+ * recorded as the empty string, and never as a present-but-`undefined` key.
12991
+ *
12992
+ * The consumers of `ResourceState.attributes` all key off exactly that
12993
+ * distinction, and a sentinel puts every one of them on the wrong branch:
12994
+ *
12995
+ * - the deploy engine persists the map as-is, so `''` becomes the recorded
12996
+ * value of a field AWS never assigned;
12997
+ * - the intrinsic resolver serves a stored attribute whenever it is not
12998
+ * `undefined`, so `''` shadows `constructAttribute` forever — the
12999
+ * `AWS::EC2::Instance` IP / DNS / AZ arm re-describes the instance from
13000
+ * AWS only on a MISS, the `Arn` arms of the RDS family rebuild the ARN
13001
+ * only on a MISS;
13002
+ * - the Outputs export pass publishes whatever the resolver returned, so a
13003
+ * stored `''` is exported to every `Fn::ImportValue` consumer.
13004
+ *
13005
+ * It exists as a MODULE rather than a per-provider idiom because the rule was
13006
+ * prose and 96 attribute-position `?? ''` / `|| ''` / bare `''` sites across
13007
+ * 18 providers shipped anyway (measured 2026-09-14 with the TypeScript-compiler
13008
+ * walk that is now `tests/unit/provisioning/attribute-map.test.ts`). A helper
13009
+ * that DROPS the key is the one form a provider cannot spell wrong by habit.
13010
+ *
13011
+ * Two values are treated as "not read back":
13012
+ *
13013
+ * - `undefined`: the SDK's spelling of an unassigned optional member.
13014
+ * - `null`: no AWS SDK v3 read-back reports a scalar attribute as `null`, and
13015
+ * a stored `null` takes the resolver's "not `undefined`" hit branch exactly
13016
+ * like `''` does.
13017
+ *
13018
+ * Every other value is kept verbatim — INCLUDING an empty string AWS itself
13019
+ * reported (`false`, `0`, an empty array and an object likewise). The helper
13020
+ * stops cdkd MANUFACTURING `''` for a field it never read; a `''` that came
13021
+ * off the wire is a known value with a meaning the provider owns. The EC2
13022
+ * Instance provider is where that distinction is load-bearing: a `running`
13023
+ * instance with no public address is described with `PublicDnsName: ''` and
13024
+ * no `PublicIpAddress`, and CloudFormation answers `''` for both — a KNOWN
13025
+ * empty, recorded as such so a reference does not degrade to the instance id
13026
+ * on every resolution — while the same `''` on a `pending` instance means
13027
+ * "not yet", which that provider maps to `undefined` BEFORE calling this
13028
+ * helper (its `describedInstanceAttributes`). A caller that knows a `''`
13029
+ * means "unknown" is the one that must say so; this helper cannot tell.
13030
+ *
13031
+ * Deliberately NOT a general "compact this record" utility: it is scoped to
13032
+ * attribute maps. A property bag keeps its `undefined`-valued keys out by other
13033
+ * means (`.claude/rules/provider-property-fidelity.md`, "Remove the key rather
13034
+ * than setting it to `undefined`").
13035
+ *
13036
+ * A LEAF module (imports nothing) so every provider can take it without adding
13037
+ * an edge to the provider import ring.
13038
+ */
13039
+ /**
13040
+ * Build an attribute map from read-back values, omitting every key whose value
13041
+ * is `undefined` or `null`.
13042
+ */
13043
+ function definedAttributes(entries) {
13044
+ const out = {};
13045
+ for (const [key, value] of Object.entries(entries)) {
13046
+ if (value === void 0 || value === null) continue;
13047
+ out[key] = value;
13048
+ }
13049
+ return out;
13050
+ }
13051
+ /**
13052
+ * `String(value)` for a numeric read-back whose CloudFormation attribute is a
13053
+ * string (`Endpoint.Port`), or `undefined` when the field is unassigned — so
13054
+ * `definedAttributes` drops the key instead of recording the literal
13055
+ * `'undefined'`, which is what `String(described?.Port ?? '')` yields once its
13056
+ * `?? ''` is merely deleted.
13057
+ */
13058
+ function stringifyIfAssigned(value) {
13059
+ if (value === void 0 || value === null) return void 0;
13060
+ return String(value);
13061
+ }
13062
+
12977
13063
  //#endregion
12978
13064
  //#region src/provisioning/providers/sns-subscription-provider.ts
12979
13065
  /**
@@ -13046,17 +13132,18 @@ var SNSSubscriptionProvider = class {
13046
13132
  if (replayPolicy !== void 0) attributes["ReplayPolicy"] = typeof replayPolicy === "string" ? replayPolicy : JSON.stringify(replayPolicy);
13047
13133
  const subscriptionRoleArn = properties["SubscriptionRoleArn"];
13048
13134
  if (subscriptionRoleArn !== void 0) attributes["SubscriptionRoleArn"] = stringifyValue(subscriptionRoleArn);
13049
- const subscriptionArn = (await this.snsClient.send(new SubscribeCommand({
13135
+ const response = await this.snsClient.send(new SubscribeCommand({
13050
13136
  TopicArn: topicArn,
13051
13137
  Protocol: protocol,
13052
13138
  Endpoint: endpoint,
13053
13139
  ReturnSubscriptionArn: true,
13054
13140
  ...Object.keys(attributes).length > 0 && { Attributes: attributes }
13055
- }))).SubscriptionArn || `${topicArn}:${logicalId}`;
13141
+ }));
13142
+ const subscriptionArn = response.SubscriptionArn || `${topicArn}:${logicalId}`;
13056
13143
  this.logger.debug(`Successfully created SNS subscription ${logicalId}: ${subscriptionArn}`);
13057
13144
  return {
13058
13145
  physicalId: subscriptionArn,
13059
- attributes: {}
13146
+ attributes: definedAttributes({ Arn: response.SubscriptionArn?.startsWith("arn:") ? response.SubscriptionArn : void 0 })
13060
13147
  };
13061
13148
  } catch (error) {
13062
13149
  const cause = error instanceof Error ? error : void 0;
@@ -17748,6 +17835,20 @@ function hasUsableDeclaredCapacity(entry) {
17748
17835
  */
17749
17836
  const PROVISIONED_CAPACITY_MEMBERS = ["ReadCapacityUnits", "WriteCapacityUnits"];
17750
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
+ /**
17751
17852
  * The capacity both TABLE-level forwarders substitute for a member the
17752
17853
  * template does not declare at all — `create()`'s `CreateTable` and the
17753
17854
  * BillingMode flip's `UpdateTable`. It is the pre-issue-#3147 `?? 5`, kept
@@ -18207,14 +18308,20 @@ function reverseMapSecondaryIndex(live, desired, bagInformative) {
18207
18308
  * in PR review round 6; the carve-out is gone because the divergence is.
18208
18309
  * - `ProvisionedThroughput` / `OnDemandThroughput` keep TRUTHINESS, which is
18209
18310
  * still what their write path does: `applyGsiUpdates` gates on
18210
- * `gsi.ProvisionedThroughput` alone, `OnDemandThroughput` is forwarded
18211
- * verbatim on every path, and the `ProvisionedThroughput` coercion issue
18212
- * [#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
18213
18314
  * per-index send sites (`create()`'s mapper plus `applyGsiUpdates`'
18214
- * adopted-repair, `Create` and same-name `Update` actions) never
18215
- * SUPPRESSES the block — it rewrites members inside a block that is sent
18216
- * either way, so "would cdkd send this block" is unchanged by it, even
18217
- * 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
18218
18325
  * `isSendableWarmThroughput` over them would be wrong twice over — it reads
18219
18326
  * members those blocks do not have (`ReadUnitsPerSecond` vs
18220
18327
  * `ReadCapacityUnits`), so every declared capacity would read as undeclared.
@@ -18412,7 +18519,7 @@ var DynamoDBTableProvider = class {
18412
18519
  WriteCapacityUnits: tableCapacityForSend(pt, "WriteCapacityUnits", onUnusable)
18413
18520
  };
18414
18521
  }
18415
- if (properties["OnDemandThroughput"]) createParams.OnDemandThroughput = properties["OnDemandThroughput"];
18522
+ if (properties["OnDemandThroughput"]) createParams.OnDemandThroughput = this.coerceOnDemandCeilingsForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["OnDemandThroughput"], maskSecrets);
18416
18523
  const createWarmThroughput = this.coerceWarmThroughputForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"], maskSecrets);
18417
18524
  if (createWarmThroughput) createParams.WarmThroughput = createWarmThroughput;
18418
18525
  else this.warnRefusedWarmThroughput(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"], maskSecrets);
@@ -18581,7 +18688,7 @@ var DynamoDBTableProvider = class {
18581
18688
  if (properties["OnDemandThroughput"]) {
18582
18689
  await this.dynamoDBClient.send(new UpdateTableCommand({
18583
18690
  TableName: physicalId,
18584
- OnDemandThroughput: properties["OnDemandThroughput"]
18691
+ OnDemandThroughput: this.coerceOnDemandCeilingsForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["OnDemandThroughput"], maskSecrets)
18585
18692
  }));
18586
18693
  await this.waitForTableActiveAfterUpdate(physicalId);
18587
18694
  this.logger.debug(`Updated OnDemandThroughput on DynamoDB table ${physicalId}`);
@@ -19251,7 +19358,7 @@ var DynamoDBTableProvider = class {
19251
19358
  KeySchema: gsi.KeySchema,
19252
19359
  Projection: gsi.Projection,
19253
19360
  ...gsi.ProvisionedThroughput ? { ProvisionedThroughput: this.indexCapacityForSend(this.indexScopeAt(name, physicalId, maskSecrets), gsi.ProvisionedThroughput, maskSecrets) } : {},
19254
- ...gsi.OnDemandThroughput ? { OnDemandThroughput: gsi.OnDemandThroughput } : {},
19361
+ ...gsi.OnDemandThroughput ? { OnDemandThroughput: this.coerceOnDemandCeilingsForSend(this.indexScopeAt(name, physicalId, maskSecrets), gsi.OnDemandThroughput, maskSecrets) } : {},
19255
19362
  ...createWarm ? { WarmThroughput: createWarm } : {}
19256
19363
  } });
19257
19364
  } else {
@@ -19360,16 +19467,20 @@ var DynamoDBTableProvider = class {
19360
19467
  return this.coerceIndexCapacityBlock(scope, declared, maskSecrets) ?? declared;
19361
19468
  }
19362
19469
  /**
19363
- * One `CreateTable` index entry with BOTH of its throughput blocks coerced —
19364
- * `WarmThroughput` (PR review round 6) and `ProvisionedThroughput` (issue
19365
- * [#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)).
19366
19475
  *
19367
19476
  * `create()` forwards the declared `GlobalSecondaryIndexes` array to
19368
19477
  * `CreateTable`, so it is a SEND SITE for the same per-index values the
19369
19478
  * update path coerces. `WarmThroughput` was the one the round-5 sweep
19370
19479
  * missed; `ProvisionedThroughput` was missed by round 6 AND by issue #3147's
19371
- * own enumeration, because this forwarder is a CAST rather than a call, so
19372
- * 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
19373
19484
  * ({@link readCapacityNumber}) has been on the grammar since #3147, so until
19374
19485
  * now create and flip disagreed about the same property: a CFn-legal
19375
19486
  * `ReadCapacityUnits: '7'` reached `CreateTable` as a STRING in a `number`
@@ -19381,18 +19492,20 @@ var DynamoDBTableProvider = class {
19381
19492
  * and a provider that edits it in place would change what state reports cdkd
19382
19493
  * sent.
19383
19494
  *
19384
- * An entry that is not a plain object, and one declaring NEITHER throughput
19385
- * block, is returned UNCHANGED — identity included. An entry that DOES
19386
- * declare one is rebuilt whenever that block's coercion changes anything;
19387
- * the `WarmThroughput` half rebuilds even for an already-numeric value,
19388
- * 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.
19389
19501
  */
19390
19502
  coerceIndexThroughputForCreate(logicalId, entry, maskSecrets = (text) => text) {
19391
19503
  if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
19392
19504
  const bag = entry;
19393
19505
  const declaredWarm = bag["WarmThroughput"];
19394
19506
  const declaredCapacity = bag["ProvisionedThroughput"];
19395
- if (declaredWarm === void 0 && !isPlainCapacityBlock(declaredCapacity)) return entry;
19507
+ const declaredOnDemand = bag["OnDemandThroughput"];
19508
+ if (declaredWarm === void 0 && !isPlainCapacityBlock(declaredCapacity) && !isPlainCapacityBlock(declaredOnDemand)) return entry;
19396
19509
  const scope = this.indexScope(entry.IndexName, logicalId, maskSecrets);
19397
19510
  let out = entry;
19398
19511
  if (declaredWarm !== void 0) {
@@ -19411,6 +19524,13 @@ var DynamoDBTableProvider = class {
19411
19524
  ...out,
19412
19525
  ProvisionedThroughput: capacity
19413
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
+ }
19414
19534
  return out;
19415
19535
  }
19416
19536
  /**
@@ -19452,9 +19572,14 @@ var DynamoDBTableProvider = class {
19452
19572
  * the template had declared it, with no later call to correct it. Only
19453
19573
  * the TABLE level defaults.
19454
19574
  * - **A block that is not a plain object is forwarded VERBATIM**, the
19455
- * fail-OPEN direction this file takes everywhere: an unresolved intrinsic
19456
- * or a mis-nested template value is AWS's to reject by name, and that is
19457
- * 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}.
19458
19583
  *
19459
19584
  * A member name the grammar does not know is preserved untouched, for the
19460
19585
  * same fail-open reason — a misspelled `ReadCapacityUnit` reaches AWS and is
@@ -19477,6 +19602,178 @@ var DynamoDBTableProvider = class {
19477
19602
  return rewritten;
19478
19603
  }
19479
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
+ /**
19480
19777
  * Announce a `ProvisionedThroughput` member a forwarder DROPPED (issue
19481
19778
  * [#3147](https://github.com/go-to-k/cdkd/issues/3147); widened to the
19482
19779
  * per-index create forwarder by issue
@@ -26468,88 +26765,6 @@ var EventBridgeRuleProvider = class {
26468
26765
  }
26469
26766
  };
26470
26767
 
26471
- //#endregion
26472
- //#region src/provisioning/attribute-map.ts
26473
- /**
26474
- * The ONE spelling of the attribute-map rule an SDK provider's `create()` /
26475
- * `update()` / `import()` result follows (issue #3077, closing the `?? ''`
26476
- * shape `docs/provider-development.md` forbids under "Never store an
26477
- * empty-string placeholder"):
26478
- *
26479
- * an attribute cdkd could not read back is ABSENT from the map — never
26480
- * recorded as the empty string, and never as a present-but-`undefined` key.
26481
- *
26482
- * The consumers of `ResourceState.attributes` all key off exactly that
26483
- * distinction, and a sentinel puts every one of them on the wrong branch:
26484
- *
26485
- * - the deploy engine persists the map as-is, so `''` becomes the recorded
26486
- * value of a field AWS never assigned;
26487
- * - the intrinsic resolver serves a stored attribute whenever it is not
26488
- * `undefined`, so `''` shadows `constructAttribute` forever — the
26489
- * `AWS::EC2::Instance` IP / DNS / AZ arm re-describes the instance from
26490
- * AWS only on a MISS, the `Arn` arms of the RDS family rebuild the ARN
26491
- * only on a MISS;
26492
- * - the Outputs export pass publishes whatever the resolver returned, so a
26493
- * stored `''` is exported to every `Fn::ImportValue` consumer.
26494
- *
26495
- * It exists as a MODULE rather than a per-provider idiom because the rule was
26496
- * prose and 96 attribute-position `?? ''` / `|| ''` / bare `''` sites across
26497
- * 18 providers shipped anyway (measured 2026-09-14 with the TypeScript-compiler
26498
- * walk that is now `tests/unit/provisioning/attribute-map.test.ts`). A helper
26499
- * that DROPS the key is the one form a provider cannot spell wrong by habit.
26500
- *
26501
- * Two values are treated as "not read back":
26502
- *
26503
- * - `undefined`: the SDK's spelling of an unassigned optional member.
26504
- * - `null`: no AWS SDK v3 read-back reports a scalar attribute as `null`, and
26505
- * a stored `null` takes the resolver's "not `undefined`" hit branch exactly
26506
- * like `''` does.
26507
- *
26508
- * Every other value is kept verbatim — INCLUDING an empty string AWS itself
26509
- * reported (`false`, `0`, an empty array and an object likewise). The helper
26510
- * stops cdkd MANUFACTURING `''` for a field it never read; a `''` that came
26511
- * off the wire is a known value with a meaning the provider owns. The EC2
26512
- * Instance provider is where that distinction is load-bearing: a `running`
26513
- * instance with no public address is described with `PublicDnsName: ''` and
26514
- * no `PublicIpAddress`, and CloudFormation answers `''` for both — a KNOWN
26515
- * empty, recorded as such so a reference does not degrade to the instance id
26516
- * on every resolution — while the same `''` on a `pending` instance means
26517
- * "not yet", which that provider maps to `undefined` BEFORE calling this
26518
- * helper (its `describedInstanceAttributes`). A caller that knows a `''`
26519
- * means "unknown" is the one that must say so; this helper cannot tell.
26520
- *
26521
- * Deliberately NOT a general "compact this record" utility: it is scoped to
26522
- * attribute maps. A property bag keeps its `undefined`-valued keys out by other
26523
- * means (`.claude/rules/provider-property-fidelity.md`, "Remove the key rather
26524
- * than setting it to `undefined`").
26525
- *
26526
- * A LEAF module (imports nothing) so every provider can take it without adding
26527
- * an edge to the provider import ring.
26528
- */
26529
- /**
26530
- * Build an attribute map from read-back values, omitting every key whose value
26531
- * is `undefined` or `null`.
26532
- */
26533
- function definedAttributes(entries) {
26534
- const out = {};
26535
- for (const [key, value] of Object.entries(entries)) {
26536
- if (value === void 0 || value === null) continue;
26537
- out[key] = value;
26538
- }
26539
- return out;
26540
- }
26541
- /**
26542
- * `String(value)` for a numeric read-back whose CloudFormation attribute is a
26543
- * string (`Endpoint.Port`), or `undefined` when the field is unassigned — so
26544
- * `definedAttributes` drops the key instead of recording the literal
26545
- * `'undefined'`, which is what `String(described?.Port ?? '')` yields once its
26546
- * `?? ''` is merely deleted.
26547
- */
26548
- function stringifyIfAssigned(value) {
26549
- if (value === void 0 || value === null) return void 0;
26550
- return String(value);
26551
- }
26552
-
26553
26768
  //#endregion
26554
26769
  //#region src/provisioning/providers/eventbridge-bus-provider.ts
26555
26770
  /**
@@ -62205,6 +62420,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62205
62420
  const guardIndeterminateTargets = /* @__PURE__ */ new Set();
62206
62421
  const regionForState = state.region ?? ctx.baseRegion;
62207
62422
  refuseMalformedResourcesForDestroy(state, stackName, regionForState);
62423
+ refuseDivergentRecordRegionForDestroy(state, stackName, regionForState, ctx.divergentBodyRegion);
62208
62424
  const resourceCount = Object.keys(state.resources).length;
62209
62425
  const orphanCount = (state.orphans ?? []).length;
62210
62426
  refuseMalformedOutputsForDestroy(state, stackName, regionForState);
@@ -62264,7 +62480,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62264
62480
  try {
62265
62481
  await ctx.lockManager.releaseLock(stackName, regionForState);
62266
62482
  } catch (releaseErr) {
62267
- 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}`);
62268
62484
  } finally {
62269
62485
  process.removeListener("SIGINT", emptySigintHandler);
62270
62486
  }
@@ -62323,7 +62539,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62323
62539
  try {
62324
62540
  switchedClients?.destroy();
62325
62541
  } catch (destroyError) {
62326
- 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}`);
62327
62543
  }
62328
62544
  };
62329
62545
  if (stackRegion && stackRegion !== ctx.baseRegion) {
@@ -62392,7 +62608,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62392
62608
  try {
62393
62609
  await ctx.lockManager.releaseLock(stackName, regionForState);
62394
62610
  } catch (releaseErr) {
62395
- 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}`);
62396
62612
  }
62397
62613
  restoreBaseRegionAndClients();
62398
62614
  process.removeListener("SIGINT", sigintHandler);
@@ -62432,7 +62648,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62432
62648
  await ctx.stateBackend.saveState(stackName, regionForState, buildDestroySnapshot());
62433
62649
  logger.debug(`State persisted after deleting ${logicalId}`);
62434
62650
  } catch (error) {
62435
- 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}`);
62436
62652
  }
62437
62653
  });
62438
62654
  };
@@ -62628,7 +62844,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62628
62844
  persistStateAfterDelete(logicalId);
62629
62845
  } catch (error) {
62630
62846
  renderer.removeTask(logicalId);
62631
- const msg = error instanceof Error ? error.message : String(error);
62847
+ const msg = describeAwsFailure(error).detail;
62632
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"))) {
62633
62849
  logger.debug(` ${logicalId} already deleted, removing from state`);
62634
62850
  result.deletedCount++;
@@ -62660,7 +62876,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62660
62876
  error: extractDeploymentEventError(wrapped)
62661
62877
  });
62662
62878
  } else {
62663
- logger.error(` ✗ Failed to delete ${logicalId}:`, String(error));
62879
+ logger.error(` ✗ Failed to delete ${logicalId}:`, safeStringify(error));
62664
62880
  result.errorCount++;
62665
62881
  failedStateTargets.add(stateTargetFor(logicalId, resource.resourceType));
62666
62882
  ctx.eventRecorder?.record({
@@ -62692,7 +62908,7 @@ async function runDestroyForStack(stackName, state, ctx) {
62692
62908
  try {
62693
62909
  await ctx.stateBackend.saveState(stackName, regionForState, buildDestroySnapshot());
62694
62910
  } catch (error) {
62695
- 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.`);
62696
62912
  }
62697
62913
  if (result.interrupted) logger.warn(`Destroy interrupted — ${Object.keys(remainingResources).length} resource(s) not deleted. State preserved.`);
62698
62914
  else if (result.errorCount > 0) logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
@@ -62723,14 +62939,14 @@ async function runDestroyForStack(stackName, state, ctx) {
62723
62939
  try {
62724
62940
  renderer.stop();
62725
62941
  } catch (rendererError) {
62726
- 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}`);
62727
62943
  }
62728
62944
  await saveChain;
62729
62945
  logger.debug("Releasing lock...");
62730
62946
  try {
62731
62947
  await ctx.lockManager.releaseLock(stackName, regionForState);
62732
62948
  } catch (releaseErr) {
62733
- 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}`);
62734
62950
  }
62735
62951
  } finally {
62736
62952
  process.removeListener("SIGINT", sigintHandler);
@@ -63003,6 +63219,7 @@ var NestedStackProvider = class {
63003
63219
  providerRegistry: ctx.providerRegistry,
63004
63220
  baseAwsClients: ctx.awsClients,
63005
63221
  baseRegion: childRegion,
63222
+ ...childStateData.divergentBodyRegion !== void 0 && { divergentBodyRegion: childStateData.divergentBodyRegion },
63006
63223
  stateBucket: ctx.stateBucket,
63007
63224
  skipConfirmation: true,
63008
63225
  ...ctx.exportIndexStore && { exportIndexStore: ctx.exportIndexStore },
@@ -64377,7 +64594,8 @@ async function deployCommand(stacks, options) {
64377
64594
  clients: {
64378
64595
  s3: stackAwsClients.s3,
64379
64596
  cloudWatchLogs: stackAwsClients.cloudWatchLogs,
64380
- expectedRegion: stateForRecreateCheck?.state.region
64597
+ expectedRegion: stateForRecreateCheck?.state.region,
64598
+ ...stateForRecreateCheck?.divergentBodyRegion !== void 0 && { expectedRegionDiverged: true }
64381
64599
  },
64382
64600
  forceStatefulRecreation: options.forceStatefulRecreation ?? false
64383
64601
  });
@@ -74362,6 +74580,7 @@ async function destroyCommand(stackArgs, options) {
74362
74580
  providerRegistry,
74363
74581
  baseAwsClients: awsClients,
74364
74582
  baseRegion: region,
74583
+ ...stateResult.divergentBodyRegion !== void 0 && { divergentBodyRegion: stateResult.divergentBodyRegion },
74365
74584
  ...options.profile && { profile: options.profile },
74366
74585
  stateBucket,
74367
74586
  statePrefix: options.statePrefix,
@@ -76116,10 +76335,10 @@ async function exportCommand(stackArg, options) {
76116
76335
  }
76117
76336
  const cfnStackName = options.cfnStackName ?? resolvedStackName;
76118
76337
  const targetRegion = await pickStackRegion$1(stateBackend, resolvedStackName, synthedRegion, options.stackRegion);
76119
- logger.info(`Migrating cdkd stack '${resolvedStackName}' (${targetRegion}) → CloudFormation stack '${cfnStackName}'`);
76338
+ logger.info(`Migrating cdkd stack '${resolvedStackName}' (${safeSegment(targetRegion)}) → CloudFormation stack '${cfnStackName}'`);
76120
76339
  await assertCfnStackAbsent(awsClients.cloudFormation, cfnStackName);
76121
76340
  const stateData = await stateBackend.getState(resolvedStackName, targetRegion);
76122
- 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.`);
76123
76342
  const { state, etag, migrationPending } = stateData;
76124
76343
  const exportJournal = await stateBackend.loadRollbackJournal(resolvedStackName, targetRegion);
76125
76344
  if (exportJournal && exportJournal.segments.length > 0) {
@@ -76236,7 +76455,7 @@ async function exportCommand(stackArg, options) {
76236
76455
  const phase2Note = phase2Creates.length > 0 ? ` Phase 2 will then CREATE ${phase2Creates.length} non-importable resource(s) (invoking each Custom Resource's onCreate handler).` : "";
76237
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).` : "";
76238
76457
  const unchangedClaim = recreateBeforePhase2.length > 0 ? ` All other AWS resources are unchanged on import.` : ` AWS resources are unchanged on import.`;
76239
- 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.`)) {
76240
76459
  logger.info("Migration cancelled. cdkd state and CloudFormation are unchanged.");
76241
76460
  return;
76242
76461
  }
@@ -76271,7 +76490,7 @@ async function exportCommand(stackArg, options) {
76271
76490
  logger.info(` ✓ deleted ${entry.physicalId}`);
76272
76491
  } catch (err) {
76273
76492
  const msg = err instanceof Error ? err.message : String(err);
76274
- 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.`);
76275
76494
  }
76276
76495
  }
76277
76496
  const phase2Count = phase2Creates.length + recreateBeforePhase2.length;
@@ -76290,10 +76509,10 @@ async function exportCommand(stackArg, options) {
76290
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` : "";
76291
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).
76292
76511
  2. Re-run the phase 2 UPDATE manually with the full synth template:
76293
- 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.`);
76294
76513
  }
76295
76514
  await stateBackend.deleteState(resolvedStackName, targetRegion);
76296
- 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.`);
76297
76516
  printNextSteps({
76298
76517
  cfnStackName,
76299
76518
  cdkStackName: resolvedStackName,
@@ -76321,7 +76540,7 @@ async function pickStackRegion$1(stateBackend, stackName, synthRegion, flag) {
76321
76540
  }
76322
76541
  if (flag) {
76323
76542
  if (!refs.find((r) => r.region === flag)) {
76324
- const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
76543
+ const seen = refs.map((r) => safeSegment(r.region ?? "(legacy)")).join(", ");
76325
76544
  throw new Error(`No state found for stack '${stackName}' in region '${flag}'. Available regions: ${seen}.`);
76326
76545
  }
76327
76546
  return flag;
@@ -76330,7 +76549,7 @@ async function pickStackRegion$1(stateBackend, stackName, synthRegion, flag) {
76330
76549
  if (refs.find((r) => r.region === synthRegion)) return synthRegion;
76331
76550
  }
76332
76551
  if (refs.length === 1) return refs[0].region ?? synthRegion ?? "";
76333
- const regions = refs.map((r) => r.region ?? "(legacy)").join(", ");
76552
+ const regions = refs.map((r) => safeSegment(r.region ?? "(legacy)")).join(", ");
76334
76553
  throw new Error(`Stack '${stackName}' has state in multiple regions: ${regions}. Re-run with --stack-region <region> to disambiguate.`);
76335
76554
  }
76336
76555
  function parseTemplateFile(path) {
@@ -76394,9 +76613,81 @@ function isPhase2CreatableType(resourceType) {
76394
76613
  * Module-scoped rather than local to the walker: the first cut of this change
76395
76614
  * guarded the walker's two refusals and left the root one, which is the shape
76396
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.
76397
76632
  */
76398
76633
  function safeSegment(value) {
76399
- 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))}`;
76400
76691
  }
76401
76692
  /**
76402
76693
  * Recursively load the cdkd-state tree rooted at `(rootStackName, region)`.
@@ -76451,8 +76742,8 @@ async function walkCdkdStateStackTree(stackName, region, state, stateBackend) {
76451
76742
  if (entry?.resourceType !== "AWS::CloudFormation::Stack") continue;
76452
76743
  const childStackName = `${stackName}~${logicalId}`;
76453
76744
  const childResult = await stateBackend.getState(childStackName, region);
76454
- 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.`);
76455
- 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.`);
76456
76747
  nestedChildren.set(logicalId, await walkCdkdStateStackTree(childStackName, region, childResult.state, stateBackend));
76457
76748
  }
76458
76749
  return {
@@ -76533,7 +76824,7 @@ function flattenCdkdStateTreeRootFirst(tree) {
76533
76824
  */
76534
76825
  function cdkd2cfnStackName(cdkdStackName) {
76535
76826
  const cfnName = cdkdStackName.replace(/~/g, "-");
76536
- 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).`);
76537
76828
  return cfnName;
76538
76829
  }
76539
76830
  /**
@@ -76559,7 +76850,7 @@ function parseCfnChildStackNameOverrides(values) {
76559
76850
  if (!cdkdName) throw new Error(`--cfn-child-stack-name '${raw}' has an empty cdkd stack name.`);
76560
76851
  if (!cfnName) throw new Error(`--cfn-child-stack-name '${raw}' has an empty CFn stack name.`);
76561
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 '.').`);
76562
- 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.`);
76563
76854
  out.set(cdkdName, cfnName);
76564
76855
  }
76565
76856
  return out;
@@ -76645,7 +76936,7 @@ function indexNestedTemplatePaths(template, templateDir) {
76645
76936
  * orchestrator.
76646
76937
  */
76647
76938
  function buildPerStackImportNodes(rootStackName, rootTemplate, rootNestedTemplatePaths, rootTemplateFormat, tree) {
76648
- 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)}'.`);
76649
76940
  const out = /* @__PURE__ */ new Map();
76650
76941
  walk(tree, rootTemplate, rootNestedTemplatePaths, rootTemplateFormat);
76651
76942
  return out;
@@ -76659,7 +76950,7 @@ function buildPerStackImportNodes(rootStackName, rootTemplate, rootNestedTemplat
76659
76950
  });
76660
76951
  for (const [childLogicalId, childNode] of node.nestedChildren) {
76661
76952
  const childTemplatePath = nodeNestedTemplatePaths[childLogicalId];
76662
- 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)}`);
76663
76954
  const { template: childTemplate, format: childFormat } = readNestedChildTemplateFile(childTemplatePath, childLogicalId);
76664
76955
  walk(childNode, childTemplate, indexNestedTemplatePaths(childTemplate, path.dirname(childTemplatePath)), childFormat);
76665
76956
  }
@@ -76711,7 +77002,7 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
76711
77002
  blocked.push({
76712
77003
  logicalId,
76713
77004
  resourceType,
76714
- 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.`
76715
77006
  });
76716
77007
  continue;
76717
77008
  }
@@ -77404,13 +77695,13 @@ function reportDriftBaselineGaps(state, logger) {
77404
77695
  const missing = entries.filter(([, r]) => r.observedProperties === void 0);
77405
77696
  if (missing.length === 0) return;
77406
77697
  if (state.version !== void 0 && state.version < 3) {
77407
- 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.`);
77408
77699
  return;
77409
77700
  }
77410
77701
  const refused = missing.filter(([, r]) => r.observedBaselineRefused === true);
77411
77702
  const refreshable = missing.filter(([, r]) => r.observedBaselineRefused !== true);
77412
77703
  if (refreshable.length > 0) {
77413
- 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.`);
77414
77705
  for (const [logicalId] of refreshable.slice(0, 10)) logger.warn(` ${logicalId}`);
77415
77706
  if (refreshable.length > 10) logger.warn(` ... and ${refreshable.length - 10} more`);
77416
77707
  }
@@ -77953,12 +78244,12 @@ async function buildResolvedParametersPerStack(args) {
77953
78244
  for (const ref of flattenCdkdStateTreeRootFirst(args.tree)) {
77954
78245
  if (ref.stackName === args.rootStackName) continue;
77955
78246
  const node = nodeByName.get(ref.stackName);
77956
- 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.`);
77957
78248
  const parentLogicalId = node.state.parentLogicalId;
77958
78249
  const parentStackName = node.state.parentStack;
77959
- 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.`);
77960
78251
  const parentNode = nodeByName.get(parentStackName);
77961
- 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.`);
77962
78253
  const parentParamValues = {};
77963
78254
  for (const p of paramsByCdkdName.get(parentStackName) ?? []) if (p.ParameterKey !== void 0) parentParamValues[p.ParameterKey] = p.ParameterValue;
77964
78255
  const parentResolverContext = {
@@ -78043,7 +78334,7 @@ async function runPerStackImportLoop(args) {
78043
78334
  };
78044
78335
  const nodesByCdkdName = buildPerStackImportNodes(rootStackName, rootTemplate, rootStackInfoNestedTemplates, rootTemplateFormat, tree);
78045
78336
  const leafFirst = flattenCdkdStateTreeLeafFirst(tree);
78046
- 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.`);
78047
78338
  const perStackPlans = [];
78048
78339
  for (const node of leafFirst) {
78049
78340
  const meta = nodesByCdkdName.get(node.stackName);
@@ -78054,7 +78345,7 @@ async function runPerStackImportLoop(args) {
78054
78345
  });
78055
78346
  if (plan.blocked.length > 0) {
78056
78347
  const lines = groupBlockedReasons(plan.blocked);
78057
- 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")}`);
78058
78349
  }
78059
78350
  perStackPlans.push({
78060
78351
  cdkdName: meta.cdkdStackName,
@@ -78073,7 +78364,7 @@ async function runPerStackImportLoop(args) {
78073
78364
  const totalRecreate = perStackPlans.reduce((acc, p) => acc + p.recreateBeforePhase2.length, 0);
78074
78365
  logger.info("");
78075
78366
  logger.info(`Migrating cdkd nested-stack tree rooted at '${rootStackName}' → CloudFormation (${perStackPlans.length} stack(s), leaf-first):`);
78076
- 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` : ""));
78077
78368
  logger.info("");
78078
78369
  if (totalPhase2Creates > 0 && !options.includeNonImportable) {
78079
78370
  if (options.dryRun) {
@@ -78097,7 +78388,7 @@ async function runPerStackImportLoop(args) {
78097
78388
  if (!options.yes) {
78098
78389
  const phase2Note = totalPhase2Creates > 0 ? ` Phase 2 will CREATE ${totalPhase2Creates} non-importable resource(s) across the tree.` : "";
78099
78390
  const recreateNote = totalRecreate > 0 ? ` cdkd will also pre-DELETE + re-CREATE ${totalRecreate} IMPORT-unsupported resource(s) (brief unavailability window per resource).` : "";
78100
- 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.")) {
78101
78392
  logger.info("Migration cancelled. cdkd state and CloudFormation are unchanged.");
78102
78393
  return {
78103
78394
  outcome: "cancelled",
@@ -78143,11 +78434,11 @@ async function runPerStackImportLoop(args) {
78143
78434
  resolver: paramResolver,
78144
78435
  stateBackend: deps.stateBackend
78145
78436
  }));
78146
- 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.`);
78147
78438
  try {
78148
78439
  for (let i = 0; i < perStackPlans.length; i++) {
78149
78440
  const plan = perStackPlans[i];
78150
- 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)`);
78151
78442
  const stackParameters = paramsByCdkdName.get(plan.cdkdName) ?? [];
78152
78443
  const phase1ATemplate = filterTemplateForImport(plan.template, plan.phase1Imports);
78153
78444
  const phase1AResources = plan.phase1Imports.map((entry) => ({
@@ -78156,13 +78447,13 @@ async function runPerStackImportLoop(args) {
78156
78447
  ResourceIdentifier: entry.resourceIdentifier
78157
78448
  }));
78158
78449
  const injectedDeletion = injectDeletionPolicyForImport(phase1ATemplate);
78159
- 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).`);
78160
78451
  try {
78161
78452
  await submitImportChangeSet(deps.cfnClient, plan.cfnName, phase1ATemplate, phase1AResources, stackParameters, plan.templateFormat, deps.uploadOpts);
78162
78453
  } catch (err) {
78163
- const importedSummary = importedStacks.length > 0 ? importedStacks.map((s) => `${s.cdkdStackName} → ${s.cfnStackName}`).join(", ") : "(none)";
78164
- const remainingSummary = perStackPlans.slice(i).map((p) => p.cdkdName).join(", ");
78165
- 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 });
78166
78457
  }
78167
78458
  const cfnArn = (await deps.cfnClient.send(new DescribeStacksCommand({ StackName: plan.cfnName }))).Stacks?.[0]?.StackId;
78168
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.`);
@@ -78184,7 +78475,7 @@ async function runPerStackImportLoop(args) {
78184
78475
  const phase1BResources = [];
78185
78476
  for (const row of plan.nestedStackRows) {
78186
78477
  const childArn = cfnArnByCdkdName.get(row.childStackName);
78187
- 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.`);
78188
78479
  const childCfnName = cfnStackNameOf(row.childStackName);
78189
78480
  const childTemplateBody = await fetchCfnStackTemplate(deps.cfnClient, childCfnName);
78190
78481
  const uploaded = await uploadCfnTemplate({
@@ -78197,7 +78488,7 @@ async function runPerStackImportLoop(args) {
78197
78488
  uploadCleanups.push(uploaded.cleanup);
78198
78489
  const childActualTags = (await deps.cfnClient.send(new DescribeStacksCommand({ StackName: childCfnName }))).Stacks?.[0]?.Tags ?? [];
78199
78490
  const originalRow = getResourceFromTemplate(plan.template, row.logicalId);
78200
- 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.`);
78201
78492
  const rewrittenRow = injectRetainAndRewriteTemplateUrl(originalRow, uploaded.url, childActualTags);
78202
78493
  phase1BTemplate["Resources"][row.logicalId] = rewrittenRow;
78203
78494
  rewrittenNestedRows.set(row.logicalId, rewrittenRow);
@@ -78210,8 +78501,8 @@ async function runPerStackImportLoop(args) {
78210
78501
  try {
78211
78502
  await submitImportChangeSet(deps.cfnClient, plan.cfnName, phase1BTemplate, phase1BResources, stackParameters, plan.templateFormat, deps.uploadOpts);
78212
78503
  } catch (err) {
78213
- const importedSummary = importedStacks.length > 0 ? importedStacks.map((s) => `${s.cdkdStackName} → ${s.cfnStackName}`).join(", ") : "(none)";
78214
- 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 });
78215
78506
  }
78216
78507
  logger.info(` ✓ Phase 1B: parent '${plan.cfnName}' adopted ${plan.nestedStackRows.length} nested child(ren) via UPDATE-IMPORT.`);
78217
78508
  if (plan.cdkdName !== rootStackName) {
@@ -78221,7 +78512,7 @@ async function runPerStackImportLoop(args) {
78221
78512
  }
78222
78513
  if (plan.recreateBeforePhase2.length > 0) for (const entry of plan.recreateBeforePhase2) {
78223
78514
  const handler = PRE_DELETE_HANDLERS[entry.resourceType];
78224
- 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.`);
78225
78516
  logger.info(` Pre-deleting AWS resource for ${entry.logicalId} (${entry.resourceType}) so CFn can re-CREATE in phase 2...`);
78226
78517
  await handler(entry);
78227
78518
  logger.info(` ✓ deleted ${entry.physicalId}`);
@@ -78240,21 +78531,21 @@ async function runPerStackImportLoop(args) {
78240
78531
  const stateDeletionFailures = [];
78241
78532
  for (const node of leafFirst) try {
78242
78533
  await deps.stateBackend.deleteState(node.stackName, node.region);
78243
- logger.info(`cdkd state for '${node.stackName}' (${node.region}) removed.`);
78534
+ logger.info(`cdkd state for '${safeSegment(node.stackName)}' (${safeSegment(node.region)}) removed.`);
78244
78535
  } catch (err) {
78245
78536
  stateDeletionFailures.push({
78246
78537
  stackName: node.stackName,
78247
78538
  region: node.region,
78248
78539
  reason: err instanceof Error ? err.message : String(err)
78249
78540
  });
78250
- 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)}`);
78251
78542
  }
78252
78543
  if (stateDeletionFailures.length > 0) {
78253
- const lines = stateDeletionFailures.map((f) => ` - cdkd/${f.stackName}/${f.region}/state.json: ${f.reason}`).join("\n");
78254
- 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.`);
78255
78546
  }
78256
78547
  if (sessionIntrinsicSkipped.size > 0) {
78257
- const detail = [...sessionIntrinsicSkipped.entries()].map(([cdkdName, params]) => `${cdkdName} (${params.join(", ")})`).join("; ");
78548
+ const detail = [...sessionIntrinsicSkipped.entries()].map(([cdkdName, params]) => `${safeSegment(cdkdName)} (${params.join(", ")})`).join("; ");
78258
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'.`);
78259
78550
  }
78260
78551
  return {
@@ -78268,7 +78559,7 @@ async function runPerStackImportLoop(args) {
78268
78559
  for (let i = acquiredChildLocks.length - 1; i >= 0; i--) {
78269
78560
  const lock = acquiredChildLocks[i];
78270
78561
  await deps.lockManager.releaseLock(lock.stackName, lock.region).catch((err) => {
78271
- 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)}`);
78272
78563
  });
78273
78564
  }
78274
78565
  }
@@ -79678,9 +79969,29 @@ function createStateOrphanCommand() {
79678
79969
  * state record. AWS resources are left intact.
79679
79970
  *
79680
79971
  * Region scoping: when a stack name has multiple state records spread across
79681
- * regions (PR 1 territory), `--region` selects one. With the current single-
79682
- * region-per-name layout the flag still works — it sets the AWS clients'
79683
- * 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.
79684
79995
  */
79685
79996
  async function stateDestroyCommand(stackArgs, options) {
79686
79997
  const logger = getLogger();
@@ -79827,6 +80138,7 @@ async function stateDestroyCommand(stackArgs, options) {
79827
80138
  providerRegistry,
79828
80139
  baseAwsClients: setup.awsClients,
79829
80140
  baseRegion: setup.region,
80141
+ ...stateResult.divergentBodyRegion !== void 0 && { divergentBodyRegion: stateResult.divergentBodyRegion },
79830
80142
  ...options.profile && { profile: options.profile },
79831
80143
  stateBucket: setup.bucket,
79832
80144
  statePrefix: options.statePrefix,
@@ -94394,4 +94706,4 @@ function buildProgram() {
94394
94706
 
94395
94707
  //#endregion
94396
94708
  export { buildProgram };
94397
- //# sourceMappingURL=program-BgbPkrcx.js.map
94709
+ //# sourceMappingURL=program-babtsurS.js.map