@go-to-k/cdkd 0.288.7 → 0.289.1

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 ProviderRegistry, $n as formatDockerLoginError, $r as PartialFailureError, $t as STATE_SOURCED_BASELINE_RULES, A as renderStatefulReason, An as stringifyValue, Ar as PARTITION_TABLE, At as configStringRefusal, B as red, Bn as getBootstrapMarkerKey, Br as setAwsClients, Bt as s3BucketDomainName, C as refusesFinalSnapshot, Cn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Cr as CFN_TEMPLATE_URL_LIMIT, Ct as refStateLookupFromResource, D as MULTI_REGION_RECREATE_BLOCKED_TYPES, Dn as importableOutputs, Dr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, Dt as assertRegionMatch, E as extractDeploymentEventError, En as importableOutputKeys, Er as uploadCfnTemplate, Et as resolveExplicitPhysicalId, F as formatResourceLine, Fn as rewriteTemplateAssetReferences, Ft as requireConfigString, G as isExportAliasCollision, Gn as validateContainerRepoName, Gt as DiffCalculator, H as collectDeclaredOutputNames, Hn as parseBootstrapMarker, Hr as CdkdError, Ht as s3BucketRegionalDomainName, I as bold, In as AssetModeResolver, Ir as resolveBucketRegion, It as classifyReplaySecretRegion, J as secretSafeKeyDisplay, Jn as buildDockerImage, Jr as IntrinsicResolutionRefusalError, Jt as findSilentDropProperties, K as secretBearing, Kn as buildDenyExternalAccessPolicy, Kr as DeployCancelledError, Kt as INTRINSIC_KEYS, L as cyan, Ln as BOOTSTRAP_MARKER_PREFIX, Lr as AwsClients, Lt as producerRegionsFromState, M as coerceWarmThroughput$1, Mn as buildAssetRedirectMap, Mr as derivePartitionAndUrlSuffix, Mt as replayWarn, N as isWarmThroughputDecrease, Nn as createAssetRedirectResolver, Nr as AssemblyReader, Nt as requireConfigArray, O as isStatefulRecreateTargetForReplace, On as shouldRetainResource, Or as displaySafe, Ot as coerceCfnBoolean, P as toFiniteNumber, Pn as loadPublishableAssetManifest, Pr as processStackMessages, Pt as requireConfigObject, Q as clearOnUpdateRemoval, Qn as dockerSpawnEnvWithSensitive, Qr as NestedStackChildDirectDestroyError, Qt as TemplateParser, R as gray, Rn as assertAssetBucketRegion, Rr as getAwsClients, Rt as withSharedDrainBudget, S as isFinalSnapshotError, Sn as shellQuote, Sr as CFN_TEMPLATE_BODY_LIMIT, St as parameterTypeMayLoseSecretIdentity, T as makeCanonicalizePropertiesFn, Tn as exportNamesCarriedFrom, Tr as findLargeInlineResources, Tt as normalizeAwsTagsToCfn, U as collectPublishedOutputNames, Un as readBootstrapMarkerBody, Ut as s3BucketWebsiteUrl, V as yellow, Vn as isCrossRegionRedirect, Vt as s3BucketDualStackDomainName, W as exportAliasCollisionScrubWarning, Wn as validateAssetBucketName, Wr as CrossAccountSecretRefusalError, Wt as applyRoleArnIfSet, X as IAMRoleProvider, Xn as describeDockerExecFailure, Xr as LocalStartServiceError, Xt as withRetry, Y as getCurrentResourceSecrets, Yn as describeDockerCapturedOutput, Yr as LocalInvokeBuildError$1, Yt as describeTypeWithThrottleRetry, Z as collectInlinePolicyNamesManagedBySiblings, Zn as describeDockerFailure, Zt as DagBuilder, _ as ATOMIC_FINAL_SNAPSHOT_TYPES, _i as retryClassificationText, _n as rebuildClientForBucketRegion, _r as resolveStateBucketWithDefault, _t as carriesDynamicReference, a as DeploymentEventsStore, an as dynamicReferenceTokens, ar as escapeRegExp, at as endCommandInterruptScope, b as ccRoutedFinalSnapshotError, bn as buildLockContentionMessage, br as stateBucketExistenceConfirmed, bt as getAccountInfo, c as replayFailedOperations, cn as isSingleDynamicReferenceToken, cr as getDockerImageBySourceHash, ct as startInterruptWatch, d as updatePartialReason, di as isMarkedNonRetryable, dn as recordMaskOnlyValue, dr as getDefaultStateBucketName, dt as UNSPECIFIED_SKIP_REASON, ei as ProvisioningError, en as STATE_SOURCED_CROSS_GENERATION_RULES, er as getDockerCmd, et as wouldReturnToSdkProvider, f as withResourceDeadline, fi as isRetryableTransientError, fn as recoverMaskedOutput, fr as getLegacyStateBucketName, ft as deleteIndeterminateGuards, g as bindingSkippedOutputs, gi as markRedactedCause, gn as S3StateBackend, gr as resolveSkipPrefix, gt as IntrinsicFunctionResolver, h as computeImplicitDeleteEdges, hi as markNonRetryable, hn as LockManager, hr as resolveCaptureObservedState, ht as isTerminationProtectionPropagationError, i as DeploymentEventsReader, ii as StackTerminationProtectionError, in as createSecretMasker, ir as runDockerStreaming, it as beginCommandInterruptScope, j as WARM_THROUGHPUT_MEMBERS, jn as WorkGraph, jr as canonicalizeRegion, jt as readConfigString, k as isStatefulRecreateTargetSync, kn as AssetPublisher, kr as expectedOwnerParam, kt as configBooleanRefusal, l as replayRollback, li as normalizeAwsError, ln as maskSecretsInError, lr as Synthesizer, lt as CloudControlProvider, m as IMPLICIT_DELETE_DEPENDENCIES, mi as isTransientServerError, mn as scrubResourceRecord, mr as resolveAutoAssetStorage, mt as disableInstanceApiTermination, n as DEFAULT_RESOURCE_WARN_AFTER_MS, ni as ResourceUpdateNotSupportedError, nn as TEMPLATE_SOURCED_RULES, nr as redactDockerArgvValues, nt as maskDeep, o as planFailedOps, on as errorCauseChain, or as stripControlChars, ot as interruptWatchListenerCount, p as maskingRetryLogger, pi as isThrottlingError, pn as redactSecretsForState, pr as resolveApp, pt as deleteSkipReason, q as secretBearingStateKeyWarning, qn as describeAwsFailure, qr as DynamicReferenceRegionAmbiguousError, qt as findActionableSilentDrops, r as DeployEngine, ri as StackHasActiveImportsError, rn as carriesSecretMask, rr as runDockerForeground, rt as maskerOrIdentity, s as planRollback, sn as identityKeyFor, sr as AssetManifestLoader, st as isInterruptedWaitError, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as ResourceTimeoutError, tn as STATE_SOURCED_READBACK_RULES, tr as partitionSensitiveEnv, tt as createMaskedRetryLogger, u as updatePartialMessage, ui as withErrorHandling, un as maskSecretsInText, ur as synthesisStatusMessage, ut as slowCcOperationTimeoutMs, v as PRE_DELETE_SNAPSHOT_TYPES, vn as UNRENDERABLE, vr as resolveStateBucketWithDefaultAndSource, vt as cfnRefValueFromPhysicalId, w as unsupportedFinalSnapshotError, wn as DEFAULT_STATE_PREFIX, wr as MIGRATE_TMP_PREFIX, wt as WAFv2WebACLProvider, x as createPreDeleteFinalSnapshot, xn as forceQuitRecoveryClause, xr as warnDeprecatedNoPrefixCliFlag, xt as isUnboundTemplateParameter, y as buildFinalSnapshotIdentifier, yn as buildForceUnlockCommand, yr as resolveUseCdkBootstrapAssets, yt as coerceParameterTypedValue, z as green, zn as ensureAssetStorage, zr as resetAwsClients, zt as s3BucketArn } from "./deploy-engine-BQUjVO_m.js";
2
- import { _ as withStackName, c as PATTERN_B_NAME_OPTIONS, f as generateResourceName, g as withSkipPrefix, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as getCurrentStackName, n as getLogger, o as runStackBuffered, p as generateResourceNameWithFallback, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES } from "./logger-Dw-3y48G.js";
1
+ import { $ as collectInlinePolicyNamesManagedBySiblings, $n as describeDockerCapturedOutput, $r as LocalInvokeBuildError$1, $t as DagBuilder, A as isStatefulRecreateTargetForReplace, An as orphansAfterRollback, Ar as uploadCfnTemplate, At as coerceCfnBoolean, B as gray, Bn as AssetModeResolver, Br as resolveBucketRegion, Bt as withSharedDrainBudget, C as refusesFinalSnapshot, Cn as forceQuitRecoveryClause, Cr as resolveUseCdkBootstrapAssets, Ct as isUnboundTemplateParameter, D as makeSiblingClaimReader, Dn as exportNamesCarriedFrom, Dr as CFN_TEMPLATE_URL_LIMIT, Dt as normalizeAwsTagsToCfn, E as extractDeploymentEventError, En as DEFAULT_STATE_PREFIX, Er as CFN_TEMPLATE_BODY_LIMIT, Et as WAFv2WebACLProvider, F as isWarmThroughputDecrease, Fn as WorkGraph, Fr as canonicalizeRegion, Ft as requireConfigArray, G as collectPublishedOutputNames, Gn as isCrossRegionRedirect, Gt as s3BucketWebsiteUrl, H as red, Hn as assertAssetBucketRegion, Hr as getAwsClients, Ht as s3BucketDomainName, I as toFiniteNumber, In as buildAssetRedirectMap, Ir as derivePartitionAndUrlSuffix, It as requireConfigObject, J as secretBearing, Jn as validateAssetBucketName, Jr as CrossAccountSecretRefusalError, Jt as INTRINSIC_KEYS, K as exportAliasCollisionScrubWarning, Kn as parseBootstrapMarker, Kr as CdkdError, Kt as applyRoleArnIfSet, L as formatResourceLine, Ln as createAssetRedirectResolver, Lr as AssemblyReader, Lt as requireConfigString, M as renderStatefulReason, Mn as shouldRetainResource, Mr as displaySafe, Mt as configStringRefusal, N as WARM_THROUGHPUT_MEMBERS, Nn as AssetPublisher, Nr as expectedOwnerParam, Nt as readConfigString, O as planOrphanAdoption, On as importableOutputKeys, Or as MIGRATE_TMP_PREFIX, Ot as resolveExplicitPhysicalId, P as coerceWarmThroughput$1, Pn as stringifyValue, Pr as PARTITION_TABLE, Pt as replayWarn, Q as IAMRoleProvider, Qn as buildDockerImage, Qr as IntrinsicResolutionRefusalError, Qt as withRetry, R as bold, Rn as loadPublishableAssetManifest, Rr as processStackMessages, Rt as classifyReplaySecretRegion, S as isFinalSnapshotError, Sn as buildLockContentionMessage, Sr as resolveStateBucketWithDefaultAndSource, St as getAccountInfo, T as makeCanonicalizePropertiesFn, Tn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Tr as warnDeprecatedNoPrefixCliFlag, Tt as refStateLookupFromResource, U as yellow, Un as ensureAssetStorage, Ur as resetAwsClients, Ut as s3BucketDualStackDomainName, V as green, Vn as BOOTSTRAP_MARKER_PREFIX, Vr as AwsClients, Vt as s3BucketArn, W as collectDeclaredOutputNames, Wn as getBootstrapMarkerKey, Wr as setAwsClients, Wt as s3BucketRegionalDomainName, X as secretSafeKeyDisplay, Xn as buildDenyExternalAccessPolicy, Xr as DeployCancelledError, Xt as findSilentDropProperties, Y as secretBearingStateKeyWarning, Yn as validateContainerRepoName, Yt as findActionableSilentDrops, Z as getCurrentResourceSecrets, Zn as describeAwsFailure, Zr as DynamicReferenceRegionAmbiguousError, Zt as describeTypeWithThrottleRetry, _ as ATOMIC_FINAL_SNAPSHOT_TYPES, _i as isThrottlingError, _n as LockManager, _r as resolveApp, _t as isTerminationProtectionPropagationError, a as DeploymentEventsStore, ai as ResourceTimeoutError, an as carriesSecretMask, ar as partitionSensitiveEnv, at as maskerOrIdentity, b as ccRoutedFinalSnapshotError, bi as markRedactedCause, bn as UNRENDERABLE, br as resolveSkipPrefix, bt as cfnRefValueFromPhysicalId, c as replayFailedOperations, ci as StackTerminationProtectionError, cn as errorCauseChain, cr as runDockerStreaming, ct as interruptWatchListenerCount, d as updatePartialReason, dn as maskSecretsInError, dr as AssetManifestLoader, dt as CloudControlProvider, ei as LocalStartServiceError, en as TemplateParser, er as describeDockerExecFailure, et as clearOnUpdateRemoval, f as withResourceDeadline, fn as maskSecretsInText, fr as getDockerImageBySourceHash, ft as slowCcOperationTimeoutMs, g as bindingSkippedOutputs, gi as isRetryableTransientError, gn as scrubResourceRecord, gr as getLegacyStateBucketName, gt as disableInstanceApiTermination, h as computeImplicitDeleteEdges, hi as isMarkedNonRetryable, hn as redactSecretsForState, hr as getDefaultStateBucketName, ht as deleteSkipReason, i as DeploymentEventsReader, ii as ProvisioningError, in as TEMPLATE_SOURCED_RULES, ir as getDockerCmd, it as maskDeep, j as isStatefulRecreateTargetSync, jn as orphansCarriedFrom, jr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, jt as configBooleanRefusal, k as MULTI_REGION_RECREATE_BLOCKED_TYPES, kn as importableOutputs, kr as findLargeInlineResources, kt as assertRegionMatch, l as replayRollback, ln as identityKeyFor, lr as escapeRegExp, lt as isInterruptedWaitError, m as IMPLICIT_DELETE_DEPENDENCIES, mi as withErrorHandling, mn as recoverMaskedOutput, mr as synthesisStatusMessage, mt as deleteIndeterminateGuards, n as DEFAULT_RESOURCE_WARN_AFTER_MS, ni as NestedStackChildDirectDestroyError, nn as STATE_SOURCED_CROSS_GENERATION_RULES, nr as dockerSpawnEnvWithSensitive, nt as wouldReturnToSdkProvider, o as planFailedOps, oi as ResourceUpdateNotSupportedError, on as createSecretMasker, or as redactDockerArgvValues, ot as beginCommandInterruptScope, p as maskingRetryLogger, pi as normalizeAwsError, pn as recordMaskOnlyValue, pr as Synthesizer, pt as UNSPECIFIED_SKIP_REASON, q as isExportAliasCollision, qn as readBootstrapMarkerBody, qt as DiffCalculator, r as DeployEngine, ri as PartialFailureError, rn as STATE_SOURCED_READBACK_RULES, rr as formatDockerLoginError, rt as createMaskedRetryLogger, s as planRollback, si as StackHasActiveImportsError, sn as dynamicReferenceTokens, sr as runDockerForeground, st as endCommandInterruptScope, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as STATE_SOURCED_BASELINE_RULES, tr as describeDockerFailure, tt as ProviderRegistry, u as updatePartialMessage, un as isSingleDynamicReferenceToken, ur as stripControlChars, ut as startInterruptWatch, v as PRE_DELETE_SNAPSHOT_TYPES, vi as isTransientServerError, vn as S3StateBackend, vr as resolveAutoAssetStorage, vt as IntrinsicFunctionResolver, w as unsupportedFinalSnapshotError, wn as shellQuote, wr as stateBucketExistenceConfirmed, wt as parameterTypeMayLoseSecretIdentity, x as createPreDeleteFinalSnapshot, xi as retryClassificationText, xn as buildForceUnlockCommand, xr as resolveStateBucketWithDefault, xt as coerceParameterTypedValue, y as buildFinalSnapshotIdentifier, yi as markNonRetryable, yn as rebuildClientForBucketRegion, yr as resolveCaptureObservedState, yt as carriesDynamicReference, z as cyan, zn as rewriteTemplateAssetReferences, zt as producerRegionsFromState } from "./deploy-engine-14EMrfBt.js";
2
+ import { _ as withSkipPrefix, 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 } from "./logger-D2Md1kTq.js";
3
3
  import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
4
- import { t as getCdkdVersion } from "./version-DPtrXz0m.js";
5
- import { r as protectedReplacementAdvice, t as ASGProvider } from "./asg-provider-CRxRmEzr.js";
4
+ import { t as getCdkdVersion } from "./version-BvvRGJMb.js";
5
+ import { r as protectedReplacementAdvice, t as ASGProvider } from "./asg-provider-B3stERoz.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";
@@ -692,15 +692,70 @@ const allowUnsupportedTypesOption = new Option("--allow-unsupported-types <types
692
692
  * time so the user sees the error immediately.
693
693
  */
694
694
  const RESOURCE_PROPERTY_FORMAT = /^[A-Z][A-Za-z0-9]+(::[A-Z][A-Za-z0-9]+)+:[A-Z][A-Za-z0-9]*$/;
695
- function parseAllowUnsupportedPropertiesToken(value, previous) {
695
+ /**
696
+ * The flag the errors below name themselves after.
697
+ *
698
+ * Both spellings share one parser, so the message has to say which one the user
699
+ * typed — quoting the canonical name at someone who typed the deprecated alias
700
+ * sends them looking for a flag they did not use.
701
+ */
702
+ function parseRouteEntries(value, previous, flag) {
696
703
  const parsed = value.split(",").map((s) => s.trim()).filter(Boolean);
697
704
  for (const token of parsed) {
698
- if (!RESOURCE_PROPERTY_FORMAT.test(token)) throw new Error(`Invalid --allow-unsupported-properties value "${token}": expected <ResourceType>:<PropertyName> with PascalCase on both halves (e.g. AWS::Lambda::Function:RuntimeManagementConfig).`);
699
- if (token.startsWith("Custom::")) throw new Error(`Invalid --allow-unsupported-properties value "${token}": Custom:: resources are routed through cfn-response and have no write-side silent drop at cdkd, so the flag would have no effect. Use --allow-unsupported-types for type-level escape hatches instead.`);
705
+ if (!RESOURCE_PROPERTY_FORMAT.test(token)) throw new Error(`Invalid ${flag} value "${token}": expected <ResourceType>:<PropertyName> with PascalCase on both halves (e.g. AWS::Lambda::Function:RuntimeManagementConfig).`);
706
+ if (token.startsWith("Custom::")) throw new Error(`Invalid ${flag} value "${token}": Custom:: resources are routed through cfn-response and have no write-side silent drop at cdkd, so the flag would have no effect. Use --allow-unsupported-types for type-level escape hatches instead.`);
700
707
  }
701
708
  return [...previous ?? [], ...parsed];
702
709
  }
703
- const allowUnsupportedPropertiesOption = new Option("--allow-unsupported-properties <entries>", "Comma-separated <ResourceType>:<PropertyName> tokens to accept as silently dropped at deploy time. Escape hatch — the property will NOT be written to AWS, the deployed resource will be missing the field. Example: --allow-unsupported-properties AWS::Lambda::Function:RuntimeManagementConfig,AWS::RDS::DBInstance:CACertificateIdentifier").argParser(parseAllowUnsupportedPropertiesToken);
710
+ function parsePreferSdkRouteToken(value, previous) {
711
+ return parseRouteEntries(value, previous, "--prefer-sdk-route");
712
+ }
713
+ /**
714
+ * Whether the deprecated alias has already warned this process.
715
+ *
716
+ * Commander calls the parser ONCE PER OCCURRENCE, and this flag is repeatable —
717
+ * so without this a user passing it three times gets the deprecation notice
718
+ * three times, which reads as three separate problems.
719
+ */
720
+ let deprecatedAliasWarned = false;
721
+ function parseAllowUnsupportedPropertiesToken(value, previous) {
722
+ if (!deprecatedAliasWarned) {
723
+ deprecatedAliasWarned = true;
724
+ process.stderr.write("--allow-unsupported-properties is deprecated and will be removed in the first minor release after 2026-12-01; use --prefer-sdk-route, which takes the same <ResourceType>:<PropertyName> tokens and does the same thing. The old name reads as the opposite of its behaviour: it does not make the property reach AWS, it keeps the resource on the SDK provider and accepts that the value is NOT written.\n");
725
+ }
726
+ return parseRouteEntries(value, previous, "--allow-unsupported-properties");
727
+ }
728
+ /**
729
+ * `--prefer-sdk-route` — keep a resource on its SDK provider instead of letting
730
+ * an unwired property auto-route it to Cloud Control (issue
731
+ * [#3000](https://github.com/go-to-k/cdkd/issues/3000)).
732
+ *
733
+ * RENAMED from `--allow-unsupported-properties`, which read as the opposite of
734
+ * what it does and rhymed with `--allow-unsupported-types`, a flag that runs the
735
+ * OTHER way: that one ENABLES a Cloud Control attempt, this one DISABLES the
736
+ * Cloud Control auto-route. Two independent readers took the old name backwards
737
+ * in one session, one of them while writing 44 issue bodies describing it.
738
+ *
739
+ * `prefer`, not a negative or a guarantee, and the reason is mechanical rather
740
+ * than stylistic. The allow set is per `<Type>:<Prop>` while ROUTING is per
741
+ * RESOURCE, so one un-allowed sibling drop on the same resource sends the whole
742
+ * resource to Cloud Control anyway — and Cloud Control then forwards the full
743
+ * map, so the named property reaches AWS after all. A name promising "do not
744
+ * route" would be a promise this flag cannot keep; `prefer` is the one modality
745
+ * that is true in both cases.
746
+ *
747
+ * The help text therefore carries what the name deliberately does not: that the
748
+ * values go UNWRITTEN when the preference is honoured, and that it is ignored
749
+ * when the resource routes to Cloud Control for another reason.
750
+ */
751
+ const preferSdkRouteOption = new Option("--prefer-sdk-route <entries>", "Comma-separated <ResourceType>:<PropertyName> tokens to keep on the SDK provider instead of auto-routing the resource to Cloud Control. The named values are then NOT written to AWS — the deployed resource is missing the field. Ignored for a resource that routes to Cloud Control for another reason (a sibling unwired property, or a sticky cc-api state record), where the values are written after all. Example: --prefer-sdk-route AWS::Lambda::Function:RuntimeManagementConfig").argParser(parsePreferSdkRouteToken);
752
+ /**
753
+ * The deprecated spelling. HIDDEN from `--help` so new users meet only the
754
+ * successor, but still parsed so existing scripts keep working; it warns once
755
+ * per process and feeds the SAME option name, so nothing downstream can tell
756
+ * which spelling was used.
757
+ */
758
+ const allowUnsupportedPropertiesOption = new Option("--allow-unsupported-properties <entries>", "Deprecated alias for --prefer-sdk-route.").argParser(parseAllowUnsupportedPropertiesToken).hideHelp();
704
759
  /**
705
760
  * Issue [#615] — `--recreate-via-cc-api <LogicalId>` (repeatable). Each
706
761
  * named resource is destroyed + recreated this deploy via Cloud Control
@@ -724,7 +779,7 @@ function parseRecreateViaCcApiToken(value, previous) {
724
779
  if (!LOGICAL_ID_FORMAT.test(token)) throw new Error(`Invalid --recreate-via-cc-api value "${value}": expected a CloudFormation logical id (alphanumeric, starts with a letter, max 255 chars). One --recreate-via-cc-api flag per resource — repeat the flag for additional targets.`);
725
780
  return [...previous ?? [], token];
726
781
  }
727
- const recreateViaCcApiOption = new Option("--recreate-via-cc-api <logicalId>", "Destroy + recreate the named resource (by CloudFormation logical id) via Cloud Control API in this deploy, so a top-level CFn property cdkd would otherwise silently drop reaches AWS via CC. Repeatable — pass the flag once per resource. Per-resource opt-in (no bulk / no per-stack shortcut) so the destroy-and-recreate cost is acknowledged for each target. Stateful resource types (RDS, DynamoDB, S3, EFS, ...) refuse unless --force-stateful-recreation is ALSO passed (two-flag protection). Cannot be combined with --allow-unsupported-properties on the same resource type and property.").argParser(parseRecreateViaCcApiToken);
782
+ const recreateViaCcApiOption = new Option("--recreate-via-cc-api <logicalId>", "Destroy + recreate the named resource (by CloudFormation logical id) via Cloud Control API in this deploy, so a top-level CFn property cdkd would otherwise silently drop reaches AWS via CC. Repeatable — pass the flag once per resource. Per-resource opt-in (no bulk / no per-stack shortcut) so the destroy-and-recreate cost is acknowledged for each target. Stateful resource types (RDS, DynamoDB, S3, EFS, ...) refuse unless --force-stateful-recreation is ALSO passed (two-flag protection). Cannot be combined with --prefer-sdk-route on the same resource type and property.").argParser(parseRecreateViaCcApiToken);
728
783
  /**
729
784
  * Issue [#651] — `--recreate-via-sdk-provider <LogicalId>` is the reverse
730
785
  * direction of `--recreate-via-cc-api`. Once a resource is sticky on
@@ -897,6 +952,7 @@ const deployOptions = [
897
952
  aggressiveVpcParallelOption,
898
953
  new Option("-e, --exclusively", "Only deploy requested stacks, do not include dependencies").default(false),
899
954
  allowUnsupportedTypesOption,
955
+ preferSdkRouteOption,
900
956
  allowUnsupportedPropertiesOption,
901
957
  recreateViaCcApiOption,
902
958
  recreateViaSdkProviderOption,
@@ -2758,8 +2814,8 @@ function renderRecreateTargetsErrors(validation) {
2758
2814
  }
2759
2815
  if (validation.ambiguousIntent.length > 0) {
2760
2816
  if (lines.length > 0) lines.push("");
2761
- lines.push(`Ambiguous intent — ${validation.ambiguousIntent.length} resource(s) are named in BOTH --recreate-via-cc-api and --allow-unsupported-properties with the same Type:Prop on a silent-drop property the template uses:`);
2762
- for (const overlap of validation.ambiguousIntent) lines.push(` - ${overlap.logicalId} (${overlap.resourceType}) — both --recreate-via-cc-api ${overlap.logicalId} (would migrate to CC, honoring ${overlap.property}) AND --allow-unsupported-properties ${overlap.resourceType}:${overlap.property} (would keep on SDK, accepting silent drop)`);
2817
+ lines.push(`Ambiguous intent — ${validation.ambiguousIntent.length} resource(s) are named in BOTH --recreate-via-cc-api and --prefer-sdk-route with the same Type:Prop on a silent-drop property the template uses:`);
2818
+ for (const overlap of validation.ambiguousIntent) lines.push(` - ${overlap.logicalId} (${overlap.resourceType}) — both --recreate-via-cc-api ${overlap.logicalId} (would migrate to CC, honoring ${overlap.property}) AND --prefer-sdk-route ${overlap.resourceType}:${overlap.property} (would keep on SDK, accepting silent drop)`);
2763
2819
  lines.push(` Fix: pick ONE strategy per resource.`);
2764
2820
  }
2765
2821
  if (validation.blockedStatefulTargets.length > 0) {
@@ -2809,9 +2865,9 @@ function renderRecreateTargetsErrors(validation) {
2809
2865
  }
2810
2866
  if (validation.ambiguousIntentSdk.length > 0) {
2811
2867
  if (lines.length > 0) lines.push("");
2812
- lines.push(`Inverse ambiguous intent — ${validation.ambiguousIntentSdk.length} --recreate-via-sdk-provider target(s) would IMMEDIATELY be re-routed back to Cloud Control after the recreate because their template uses silent-drop properties NOT in --allow-unsupported-properties:`);
2868
+ lines.push(`Inverse ambiguous intent — ${validation.ambiguousIntentSdk.length} --recreate-via-sdk-provider target(s) would IMMEDIATELY be re-routed back to Cloud Control after the recreate because their template uses silent-drop properties NOT in --prefer-sdk-route:`);
2813
2869
  for (const overlap of validation.ambiguousIntentSdk) lines.push(` - ${overlap.logicalId} (${overlap.resourceType}) — template uses ${overlap.property}; the default-on CC auto-route would re-route the recreated resource back to CC immediately`);
2814
- lines.push(" Fix: pass --allow-unsupported-properties <Type>:<Prop> for each silent-drop property so the recreated resource stays on SDK with the property explicitly dropped. Or drop --recreate-via-sdk-provider — the resource already routes via CC and honors the property.");
2870
+ lines.push(" Fix: pass --prefer-sdk-route <Type>:<Prop> for each silent-drop property so the recreated resource stays on SDK with the property explicitly dropped. Or drop --recreate-via-sdk-provider — the resource already routes via CC and honors the property.");
2815
2871
  }
2816
2872
  return lines.length > 0 ? lines.join("\n") : null;
2817
2873
  }
@@ -6626,6 +6682,21 @@ function configItemId(item) {
6626
6682
  if (typeof id === "string") return id;
6627
6683
  if (typeof id === "number" && Number.isFinite(id)) return String(id);
6628
6684
  }
6685
+ /**
6686
+ * Whether the template asks for a bucket-level Object Lock.
6687
+ *
6688
+ * Shared on purpose by the two sites that must agree: `create()` passes
6689
+ * `ObjectLockEnabledForBucket` to `CreateBucket` from it, and the create-path
6690
+ * Object Lock skip records `{ ObjectLockEnabled: 'Enabled' }` from it. Spelled
6691
+ * twice, the recorded value could claim a flag the bucket does not carry.
6692
+ *
6693
+ * Note the source: the BUCKET-level `ObjectLockEnabled`, never the one nested
6694
+ * inside `ObjectLockConfiguration` -- only the top-level property reaches
6695
+ * `CreateBucket`, and it is the only one AWS can report back.
6696
+ */
6697
+ function declaresBucketObjectLock(properties) {
6698
+ return properties["ObjectLockEnabled"] === true || properties["ObjectLockEnabled"] === "true";
6699
+ }
6629
6700
  function isPlainObject$1(value) {
6630
6701
  return typeof value === "object" && value !== null && !Array.isArray(value);
6631
6702
  }
@@ -8913,28 +8984,46 @@ var S3BucketProvider = class S3BucketProvider {
8913
8984
  *
8914
8985
  * CFn property: ObjectLockConfiguration
8915
8986
  * - ObjectLockEnabled: 'Enabled'
8916
- * - Rule.DefaultRetention (Mode, Days, Years)
8987
+ * - Rule.DefaultRetention (Mode, Days, Years, DefaultEventHold)
8917
8988
  * SDK: PutObjectLockConfiguration with ObjectLockConfiguration
8918
8989
  *
8919
8990
  * Note: ObjectLockEnabled at bucket level must be set at creation time.
8920
8991
  * This method only applies the rule/default retention config post-creation.
8992
+ *
8993
+ * Returns whether the Put was issued. A malformed `DefaultEventHold`
8994
+ * container SKIPS the whole Put (see the guard below), and the callers owe
8995
+ * `effectiveProperties` the previous value when it does.
8921
8996
  */
8922
- async applyObjectLockConfiguration(bucketName, config) {
8997
+ async applyObjectLockConfiguration(bucketName, config, onUnusable) {
8923
8998
  const rule = config["Rule"];
8924
8999
  const objectLockConfig = { ObjectLockEnabled: "Enabled" };
8925
9000
  if (rule) {
8926
9001
  const retention = rule["DefaultRetention"];
8927
- if (retention) objectLockConfig.Rule = { DefaultRetention: {
8928
- Mode: retention["Mode"],
8929
- Days: retention["Days"],
8930
- Years: retention["Years"]
8931
- } };
9002
+ if (retention) {
9003
+ const declaredHold = retention["DefaultEventHold"];
9004
+ let eventHold;
9005
+ if (declaredHold != null) {
9006
+ const path = "AWS::S3::Bucket ObjectLockConfiguration.Rule.DefaultRetention.DefaultEventHold";
9007
+ eventHold = onUnusable ? requireConfigObject(declaredHold, path, { onUnusable }) : requireConfigObject(declaredHold, path);
9008
+ if (eventHold === void 0) return false;
9009
+ }
9010
+ objectLockConfig.Rule = { DefaultRetention: {
9011
+ Mode: retention["Mode"],
9012
+ Days: retention["Days"],
9013
+ Years: retention["Years"],
9014
+ DefaultEventHold: eventHold ? {
9015
+ Days: eventHold["Days"],
9016
+ Years: eventHold["Years"]
9017
+ } : void 0
9018
+ } };
9019
+ }
8932
9020
  }
8933
9021
  await this.s3Client.send(new PutObjectLockConfigurationCommand({
8934
9022
  Bucket: bucketName,
8935
9023
  ObjectLockConfiguration: objectLockConfig
8936
9024
  }));
8937
9025
  this.logger.debug(`Applied object lock configuration to bucket ${bucketName}`);
9026
+ return true;
8938
9027
  }
8939
9028
  /**
8940
9029
  * Apply additional bucket configuration after creation.
@@ -9470,7 +9559,9 @@ var S3BucketProvider = class S3BucketProvider {
9470
9559
  await this.s3Client.send(new DeleteBucketReplicationCommand({ Bucket: bucketName }));
9471
9560
  this.logger.debug(`Deleted replication configuration on bucket ${bucketName}`);
9472
9561
  });
9473
- await this.diffSubConfig(bucketName, previousProperties["ObjectLockConfiguration"], properties["ObjectLockConfiguration"], async (cfg) => this.applyObjectLockConfiguration(bucketName, cfg), async () => {
9562
+ await this.diffSubConfig(bucketName, previousProperties["ObjectLockConfiguration"], properties["ObjectLockConfiguration"], async (cfg) => {
9563
+ if (!await this.applyObjectLockConfiguration(bucketName, cfg, (m) => this.logger.warn(m))) retainPrevious("ObjectLockConfiguration");
9564
+ }, async () => {
9474
9565
  await this.s3Client.send(new PutObjectLockConfigurationCommand({
9475
9566
  Bucket: bucketName,
9476
9567
  ObjectLockConfiguration: { ObjectLockEnabled: "Enabled" }
@@ -9615,7 +9706,9 @@ var S3BucketProvider = class S3BucketProvider {
9615
9706
  if (!await this.applyReplicationConfiguration(bucketName, replConfig, replayOnUnusable)) overrides.set("ReplicationConfiguration", void 0);
9616
9707
  }
9617
9708
  const objectLockConfig = properties["ObjectLockConfiguration"];
9618
- if (objectLockConfig) await this.applyObjectLockConfiguration(bucketName, objectLockConfig);
9709
+ if (objectLockConfig) {
9710
+ if (!await this.applyObjectLockConfiguration(bucketName, objectLockConfig, replayOnUnusable)) overrides.set("ObjectLockConfiguration", declaresBucketObjectLock(properties) ? { ObjectLockEnabled: "Enabled" } : void 0);
9711
+ }
9619
9712
  return overrides;
9620
9713
  }
9621
9714
  /**
@@ -9902,7 +9995,7 @@ var S3BucketProvider = class S3BucketProvider {
9902
9995
  const region = await this.getRegion();
9903
9996
  const canonicalRegion = canonicalizeRegion(region);
9904
9997
  if (canonicalRegion !== "us-east-1") createParams.CreateBucketConfiguration = { LocationConstraint: canonicalRegion };
9905
- if (properties["ObjectLockEnabled"] === true || properties["ObjectLockEnabled"] === "true") createParams.ObjectLockEnabledForBucket = true;
9998
+ if (declaresBucketObjectLock(properties)) createParams.ObjectLockEnabledForBucket = true;
9906
9999
  const preflight = canonicalizeRegion(region) === "us-east-1" ? await this.probeBucketRegion(bucketName) : { kind: "absent" };
9907
10000
  let createdNewBucket = false;
9908
10001
  try {
@@ -10527,6 +10620,12 @@ var S3BucketProvider = class S3BucketProvider {
10527
10620
  if (r.Mode !== void 0) retention["Mode"] = r.Mode;
10528
10621
  if (r.Days !== void 0) retention["Days"] = r.Days;
10529
10622
  if (r.Years !== void 0) retention["Years"] = r.Years;
10623
+ if (r.DefaultEventHold !== void 0) {
10624
+ const eventHold = {};
10625
+ if (r.DefaultEventHold.Days !== void 0) eventHold["Days"] = r.DefaultEventHold.Days;
10626
+ if (r.DefaultEventHold.Years !== void 0) eventHold["Years"] = r.DefaultEventHold.Years;
10627
+ retention["DefaultEventHold"] = eventHold;
10628
+ }
10530
10629
  out["Rule"] = { DefaultRetention: retention };
10531
10630
  }
10532
10631
  return out;
@@ -61046,8 +61145,9 @@ async function runDestroyForStack(stackName, state, ctx) {
61046
61145
  };
61047
61146
  const guardIndeterminateTargets = /* @__PURE__ */ new Set();
61048
61147
  const resourceCount = Object.keys(state.resources).length;
61148
+ const orphanCount = (state.orphans ?? []).length;
61049
61149
  const regionForState = state.region ?? ctx.baseRegion;
61050
- if (resourceCount === 0) {
61150
+ if (resourceCount === 0 && orphanCount === 0) {
61051
61151
  logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
61052
61152
  let emptyLockHeld = false;
61053
61153
  let emptyInterrupted = false;
@@ -61093,7 +61193,9 @@ async function runDestroyForStack(stackName, state, ctx) {
61093
61193
  emptyLockHeld = true;
61094
61194
  try {
61095
61195
  const recheck = await ctx.stateBackend.getState(stackName, regionForState);
61096
- if (!(!recheck || Object.keys(recheck.state.resources).length === 0)) throw new Error(`Stack '${stackName}' (${regionForState}) was empty when this run started but now has ${Object.keys(recheck.state.resources).length} resource(s) — another cdkd process deployed into it. Re-run the destroy to act on the current state.`);
61196
+ const recheckResources = recheck ? Object.keys(recheck.state.resources).length : 0;
61197
+ const recheckOrphans = recheck ? (recheck.state.orphans ?? []).length : 0;
61198
+ if (!(!recheck || recheckResources === 0 && recheckOrphans === 0)) throw new Error(`Stack '${stackName}' (${regionForState}) was empty when this run started but now has ${recheckResources} resource(s) and ${recheckOrphans} rollback-orphaned resource(s) — another cdkd process wrote to it. Re-run the destroy to act on the current state.`);
61097
61199
  await ctx.stateBackend.deleteState(stackName, regionForState);
61098
61200
  logger.info(`${green("✓")} State deleted`);
61099
61201
  } finally {
@@ -61116,6 +61218,12 @@ async function runDestroyForStack(stackName, state, ctx) {
61116
61218
  logger.info(`\nResources to be deleted (${resourceCount}):`);
61117
61219
  for (const [logicalId, resource] of Object.entries(state.resources)) logger.info(` - ${logicalId} (${resource.resourceType})`);
61118
61220
  const protectedCount = ctx.removeProtection ? countProtectedResources(state) : 0;
61221
+ const orphansAtDestroy = state.orphans ?? [];
61222
+ if (orphansAtDestroy.length > 0) {
61223
+ logger.info(`\n${orphansAtDestroy.length} resource(s) are still in AWS from an earlier failed deploy. cdkd left them there because their DeletionPolicy is Retain (CDK's RemovalPolicy.RETAIN), and has been tracking them so a later deploy of this stack could adopt them back instead of colliding with their names.`);
61224
+ logger.info("Destroying this stack deletes that record. The resources stay in AWS, may continue to incur charges, and cdkd will no longer know about them:");
61225
+ for (const entry of orphansAtDestroy) logger.info(` - ${entry.logicalId} (${entry.state.resourceType}) ${entry.state.physicalId}`);
61226
+ }
61119
61227
  if (!ctx.skipConfirmation) {
61120
61228
  if (process.stdin.isTTY !== true) throw new CdkdError(`The destroy confirmation prompt for stack "${stackName}" cannot run in a non-interactive environment. Pass --yes / -y to confirm the destroy (cdkd destroy also accepts -f / --force), or run the command from a real terminal.`, "NON_INTERACTIVE_CONFIRM");
61121
61229
  const rl = readline$1.createInterface({
@@ -63165,7 +63273,8 @@ async function deployCommand(stacks, options) {
63165
63273
  registerAllProviders(stackProviderRegistry);
63166
63274
  stackProviderRegistry.setCustomResourceResponseBucket(stateBucket);
63167
63275
  if (options.allowUnsupportedTypes?.length) stackProviderRegistry.allowUnsupportedTypes(options.allowUnsupportedTypes);
63168
- if (options.allowUnsupportedProperties?.length) stackProviderRegistry.allowUnsupportedProperties(options.allowUnsupportedProperties);
63276
+ const preferSdkRoute = [...options.preferSdkRoute ?? [], ...options.allowUnsupportedProperties ?? []];
63277
+ if (preferSdkRoute.length) stackProviderRegistry.allowUnsupportedProperties(preferSdkRoute);
63169
63278
  const eventRecorder = startRunRecorder({
63170
63279
  backend: stackStateBackend,
63171
63280
  stackName: stackInfo.stackName,
@@ -63186,7 +63295,7 @@ async function deployCommand(stacks, options) {
63186
63295
  const syncValidation = validateRecreateTargets({
63187
63296
  template: stackInfo.template,
63188
63297
  state: stateForRecreateCheck?.state ?? {
63189
- version: 9,
63298
+ version: 10,
63190
63299
  stackName: stackInfo.stackName,
63191
63300
  region: stackRegion,
63192
63301
  resources: {},
@@ -63195,7 +63304,7 @@ async function deployCommand(stacks, options) {
63195
63304
  },
63196
63305
  recreateViaCcApi: options.recreateViaCcApi ?? [],
63197
63306
  recreateViaSdkProvider: options.recreateViaSdkProvider ?? [],
63198
- allowUnsupportedProperties: new Set(options.allowUnsupportedProperties ?? []),
63307
+ allowUnsupportedProperties: /* @__PURE__ */ new Set([...options.preferSdkRoute ?? [], ...options.allowUnsupportedProperties ?? []]),
63199
63308
  forceStatefulRecreation: options.forceStatefulRecreation ?? false,
63200
63309
  hasSdkProvider: (rt) => stackProviderRegistry.getProviderType(rt) === "sdk"
63201
63310
  });
@@ -64849,7 +64958,7 @@ async function loadStateOrEmpty(stackName, region, stateBackend) {
64849
64958
  region,
64850
64959
  resources: {},
64851
64960
  outputs: {},
64852
- version: 9,
64961
+ version: 10,
64853
64962
  lastModified: Date.now()
64854
64963
  };
64855
64964
  }
@@ -64996,7 +65105,24 @@ async function computeStackDiff(currentState, template, region, stackName, state
64996
65105
  ...conditions && { conditions },
64997
65106
  skipDynamicReferences: true
64998
65107
  });
64999
- const changes = await diffCalculator.calculateDiff(currentState, effectiveTemplate, resolveFn, canonicalizeProperties);
65108
+ let adoptedOrphans = [];
65109
+ let adoptedRecords = {};
65110
+ let blocking = [];
65111
+ let stateForDiff = currentState;
65112
+ if (currentState.orphans?.length && options.previewOrphanAdoption) {
65113
+ const plan = await options.previewOrphanAdoption(currentState, effectiveTemplate, stackName, region);
65114
+ adoptedOrphans = Object.keys(plan.adopted);
65115
+ adoptedRecords = plan.adopted;
65116
+ blocking = plan.refusals;
65117
+ if (adoptedOrphans.length > 0) stateForDiff = {
65118
+ ...currentState,
65119
+ resources: {
65120
+ ...currentState.resources,
65121
+ ...plan.adopted
65122
+ }
65123
+ };
65124
+ }
65125
+ const changes = await diffCalculator.calculateDiff(stateForDiff, effectiveTemplate, resolveFn, canonicalizeProperties);
65000
65126
  const changedLogicalIds = new Set([...changes.values()].filter((c) => c.changeType !== "NO_CHANGE").map((c) => c.logicalId));
65001
65127
  const bindingSkipped = bindingSkippedOutputs(outputsDigestSource, currentState.skippedOutputs, changedLogicalIds);
65002
65128
  const resolved = await resolveTemplateOutputs(effectiveTemplate, resolveFn, conditions, currentState.outputs, bindingSkipped);
@@ -65012,7 +65138,10 @@ async function computeStackDiff(currentState, template, region, stackName, state
65012
65138
  });
65013
65139
  return {
65014
65140
  changes,
65015
- outputChanges
65141
+ outputChanges,
65142
+ adoptedOrphans,
65143
+ adoptedRecords,
65144
+ blocking
65016
65145
  };
65017
65146
  }
65018
65147
  /**
@@ -65075,22 +65204,32 @@ async function resolveChildStackParameters(parentStackRow, parentTemplate, paren
65075
65204
  * fail at deploy time.
65076
65205
  */
65077
65206
  async function buildDiffTree(args) {
65078
- const { stackName, displayName, region, template, nestedTemplates, recursive, stateBackend, diffCalculator, parentHasSecretReference, parameters, canonicalizeProperties, assetRedirect, cfnFallback } = args;
65207
+ const { stackName, displayName, region, template, nestedTemplates, recursive, stateBackend, diffCalculator, parentHasSecretReference, parameters, canonicalizeProperties, assetRedirect, cfnFallback, previewOrphanAdoption } = args;
65079
65208
  const state = await loadStateOrEmpty(stackName, region, stateBackend);
65080
65209
  const secretBearingAbove = parentHasSecretReference === true || templateHasSecretDynamicReference(template);
65081
- const { changes, outputChanges } = await computeStackDiff(state, template, region, stackName, stateBackend, diffCalculator, {
65210
+ const { changes, outputChanges, adoptedOrphans, adoptedRecords, blocking } = await computeStackDiff(state, template, region, stackName, stateBackend, diffCalculator, {
65082
65211
  ...parameters && { parameters },
65083
65212
  ...canonicalizeProperties && { canonicalizeProperties },
65084
65213
  ...cfnFallback !== void 0 && { cfnFallback },
65214
+ ...previewOrphanAdoption && { previewOrphanAdoption },
65085
65215
  inheritSecretBearingTemplate: false
65086
65216
  });
65217
+ const stateAfterAdoption = Object.keys(adoptedRecords).length > 0 ? {
65218
+ ...state,
65219
+ resources: {
65220
+ ...state.resources,
65221
+ ...adoptedRecords
65222
+ }
65223
+ } : state;
65087
65224
  const node = {
65088
65225
  stackName,
65089
65226
  displayName,
65090
65227
  region,
65091
65228
  changes,
65092
- ccApiRoutes: collectCcApiRoutes(template, state),
65229
+ ccApiRoutes: collectCcApiRoutes(template, stateAfterAdoption),
65093
65230
  outputChanges,
65231
+ adoptedOrphans,
65232
+ blocking,
65094
65233
  children: []
65095
65234
  };
65096
65235
  if (!recursive) return node;
@@ -65104,7 +65243,7 @@ async function buildDiffTree(args) {
65104
65243
  const childTemplate = readNestedTemplate(childTemplatePath);
65105
65244
  if (assetRedirect) rewriteTemplateAssetReferences(childTemplate, assetRedirect);
65106
65245
  const grandchildTemplates = indexNestedChildTemplates(childTemplate, childTemplatePath);
65107
- const childParameters = await resolveChildStackParameters(resource, template, state, region, stackName, stateBackend, parameters, cfnFallback);
65246
+ const childParameters = await resolveChildStackParameters(resource, template, stateAfterAdoption, region, stackName, stateBackend, parameters, cfnFallback);
65108
65247
  node.children.push(await buildDiffTree({
65109
65248
  stackName: childStackName,
65110
65249
  displayName: childStackName,
@@ -65118,6 +65257,7 @@ async function buildDiffTree(args) {
65118
65257
  ...canonicalizeProperties && { canonicalizeProperties },
65119
65258
  ...assetRedirect && { assetRedirect },
65120
65259
  ...cfnFallback !== void 0 && { cfnFallback },
65260
+ ...previewOrphanAdoption && { previewOrphanAdoption },
65121
65261
  parentHasSecretReference: secretBearingAbove
65122
65262
  }));
65123
65263
  }
@@ -65143,6 +65283,8 @@ async function buildDeletedSubtree(stackName, region, stateBackend, diffCalculat
65143
65283
  region,
65144
65284
  changes,
65145
65285
  ccApiRoutes: /* @__PURE__ */ new Map(),
65286
+ adoptedOrphans: [],
65287
+ blocking: [],
65146
65288
  outputChanges,
65147
65289
  children: []
65148
65290
  };
@@ -65209,9 +65351,41 @@ function collectCcApiRoutes(template, state) {
65209
65351
  /** True when this node has at least one real (non-`NO_CHANGE`) change. */
65210
65352
  function nodeHasChanges(node) {
65211
65353
  for (const change of node.changes.values()) if (change.changeType !== "NO_CHANGE") return true;
65212
- return node.outputChanges.length > 0;
65354
+ return node.outputChanges.length > 0 || node.adoptedOrphans.length > 0;
65213
65355
  }
65214
65356
  /** True when this node OR any descendant has a real change (tree-wide drift detector for `--fail`). */
65357
+ /**
65358
+ * How many deploy refusals the whole tree carries (issue go-to-k/cdkd#2943).
65359
+ *
65360
+ * A COUNT rather than a boolean because the caller's message quotes it.
65361
+ * {@link treeIsWorthRendering} below IS a boolean built on this, added when
65362
+ * review found the render gate gated on changes alone — it derives from this
65363
+ * count rather than walking the tree again, so there is still one recursion to
65364
+ * keep in step.
65365
+ *
65366
+ * Deliberately SEPARATE from {@link treeHasChanges}: `--fail` answers "is
65367
+ * there a delta", and a refusal is not a delta — a user who runs
65368
+ * `cdkd diff --fail` in CI to detect drift must not have that signal merged
65369
+ * with "the deploy cannot start", which is true whether or not anything
65370
+ * changed.
65371
+ */
65372
+ function countBlocking(node) {
65373
+ return node.blocking.length + node.children.reduce((n, c) => n + countBlocking(c), 0);
65374
+ }
65375
+ /**
65376
+ * Whether `cdkd diff` should render this tree's block at all (issue
65377
+ * go-to-k/cdkd#2943).
65378
+ *
65379
+ * A named predicate rather than an inline conjunction at the call site,
65380
+ * because the two halves are easy to get wrong INDEPENDENTLY and were:
65381
+ * {@link renderDiffTree} was written to print a `Blocking` section for a node
65382
+ * with no changes, and the caller then skipped the render on
65383
+ * `!treeHasChanges` alone — so a changeless refusal would print "No changes
65384
+ * detected" and exit non-zero citing reasons that were never printed.
65385
+ */
65386
+ function treeIsWorthRendering(node) {
65387
+ return treeHasChanges(node) || countBlocking(node) > 0;
65388
+ }
65215
65389
  function treeHasChanges(node) {
65216
65390
  if (nodeHasChanges(node)) return true;
65217
65391
  return node.children.some(treeHasChanges);
@@ -65248,6 +65422,8 @@ function diffTreeToJson(node) {
65248
65422
  ...change.changeType !== "REMOVE" ? { newValue: change.newValue } : {},
65249
65423
  export: change.isExport
65250
65424
  })),
65425
+ adoptedOrphans: node.adoptedOrphans,
65426
+ blocking: node.blocking,
65251
65427
  children: node.children.map(diffTreeToJson)
65252
65428
  };
65253
65429
  }
@@ -65340,11 +65516,22 @@ function renderDiffValue(own, filtered, indent, isNewSide) {
65340
65516
  * #614's auto-fallback decision at plan time. DELETE lines are not annotated
65341
65517
  * — the delete routing is recorded on each resource's `provisionedBy` state
65342
65518
  * field rather than re-derived from the template.
65519
+ *
65520
+ * `adoptedOrphans` annotates the rows whose resource is in this diff only
65521
+ * because a rollback-orphan record was verified and spliced in (issue
65522
+ * go-to-k/cdkd#2943). Without it the row is indistinguishable from an ordinary
65523
+ * update, and the user last saw that resource FAIL and leave state — so an
65524
+ * unannotated `[~]` reads as cdkd having quietly kept managing it. Both
65525
+ * annotations can apply to one row; adoption is printed first because it is
65526
+ * why the row exists at all, where the routing describes how it will be
65527
+ * carried out.
65343
65528
  */
65344
- function renderChangeLines(changes, logFn, ccApiRoutes) {
65529
+ function renderChangeLines(changes, logFn, ccApiRoutes, adoptedOrphans) {
65345
65530
  let createCount = 0;
65346
65531
  let updateCount = 0;
65347
65532
  let deleteCount = 0;
65533
+ const adopted = new Set(adoptedOrphans ?? []);
65534
+ const annotateAdoption = (logicalId) => adopted.has(logicalId) ? " [adopted from a rollback orphan]" : "";
65348
65535
  const annotateRouting = (logicalId) => {
65349
65536
  const props = ccApiRoutes?.get(logicalId);
65350
65537
  if (!props || props.length === 0) return "";
@@ -65354,11 +65541,11 @@ function renderChangeLines(changes, logFn, ccApiRoutes) {
65354
65541
  for (const [logicalId, change] of changes.entries()) switch (change.changeType) {
65355
65542
  case "CREATE":
65356
65543
  createCount++;
65357
- logFn(` [+] ${logicalId} (${change.resourceType})${annotateRouting(logicalId)}`);
65544
+ logFn(` [+] ${logicalId} (${change.resourceType})${annotateAdoption(logicalId)}${annotateRouting(logicalId)}`);
65358
65545
  break;
65359
65546
  case "UPDATE":
65360
65547
  updateCount++;
65361
- logFn(` [~] ${logicalId} (${change.resourceType})${annotateRouting(logicalId)}`);
65548
+ logFn(` [~] ${logicalId} (${change.resourceType})${annotateAdoption(logicalId)}${annotateRouting(logicalId)}`);
65362
65549
  if (change.propertyChanges && change.propertyChanges.length > 0) for (const propChange of change.propertyChanges) {
65363
65550
  const requiresReplace = propChange.requiresReplacement ? " [requires replacement]" : "";
65364
65551
  const propagated = propChange.replacementPropagated ? " [replacement propagated]" : "";
@@ -65466,12 +65653,18 @@ function renderOutputChangeLines(outputChanges, logFn) {
65466
65653
  * (the A4 `state show --show-nested` convention, full `~`-joined name).
65467
65654
  */
65468
65655
  function renderDiffTree(node, isRoot, logFn) {
65469
- if (nodeHasChanges(node)) {
65470
- logFn(isRoot ? `\nStack ${node.stackName}:` : `\nNested stack: ${node.displayName}`);
65471
- const { create, update, delete: del } = renderChangeLines(node.changes, logFn, node.ccApiRoutes);
65656
+ const hasChanges = nodeHasChanges(node);
65657
+ if (hasChanges || node.blocking.length > 0) logFn(isRoot ? `\nStack ${node.stackName}:` : `\nNested stack: ${node.displayName}`);
65658
+ if (hasChanges) {
65659
+ const { create, update, delete: del } = renderChangeLines(node.changes, logFn, node.ccApiRoutes, node.adoptedOrphans);
65472
65660
  const outputs = renderOutputChangeLines(node.outputChanges, logFn);
65473
65661
  logFn(`\n${create} to create, ${update} to update, ${del} to delete`);
65474
65662
  if (outputs.add + outputs.change + outputs.remove > 0) logFn(`${outputs.add} output(s) to add, ${outputs.change} to change, ${outputs.remove} to remove`);
65663
+ if (node.adoptedOrphans.length > 0) logFn(`${node.adoptedOrphans.length} resource(s) to adopt from a previous rollback: ${node.adoptedOrphans.map(stripControlChars).join(", ")}`);
65664
+ }
65665
+ if (node.blocking.length > 0) {
65666
+ logFn("\n Blocking (cdkd deploy will refuse):");
65667
+ for (const reason of node.blocking) logFn(` ! ${stripControlChars(reason)}`);
65475
65668
  }
65476
65669
  for (const child of node.children) renderDiffTree(child, false, logFn);
65477
65670
  }
@@ -65493,6 +65686,35 @@ var DiffDetectedError = class DiffDetectedError extends CdkdError {
65493
65686
  }
65494
65687
  };
65495
65688
  /**
65689
+ * Signals that the preview completed and `cdkd deploy` would REFUSE to start
65690
+ * (issue go-to-k/cdkd#2943) — today, a rollback-orphan record whose physical
65691
+ * id another cdkd stack already manages.
65692
+ *
65693
+ * Exit **3**, and neither 1 nor 2, for reasons that are about what a caller
65694
+ * can conclude:
65695
+ *
65696
+ * - **1** is `--fail`'s "a change was detected". A refusal is not a change,
65697
+ * and merging them would make a CI job that gates on drift report the same
65698
+ * code for "there is work to do" and "the work cannot begin".
65699
+ * - **2** is this CLI's partial-failure family, documented as "work
65700
+ * completed, re-running typically resolves it" (`docs/cli-reference.md`).
65701
+ * A refusal is the opposite: re-running changes nothing until a human
65702
+ * resolves the ownership conflict.
65703
+ *
65704
+ * It is NOT silent: unlike `--fail`, this is not a flag the user opted into,
65705
+ * so the reason has to be visible even when the diff report scrolled away.
65706
+ * The `Blocking` section already printed the per-record detail; this adds the
65707
+ * one line that says the run cannot proceed.
65708
+ */
65709
+ var DeployRefusalPreviewError = class DeployRefusalPreviewError extends CdkdError {
65710
+ exitCode = 3;
65711
+ constructor(count) {
65712
+ super(`cdkd deploy would refuse to start: ${count} blocking condition(s) reported above. Resolve them, or run \`cdkd state\` to inspect the conflicting stack.`, "DEPLOY_REFUSAL_PREVIEW");
65713
+ this.name = "DeployRefusalPreviewError";
65714
+ Object.setPrototypeOf(this, DeployRefusalPreviewError.prototype);
65715
+ }
65716
+ };
65717
+ /**
65496
65718
  * Diff command implementation
65497
65719
  */
65498
65720
  async function diffCommand(stacks, options) {
@@ -65553,6 +65775,34 @@ async function diffCommand(stacks, options) {
65553
65775
  const diffProviderRegistry = new ProviderRegistry();
65554
65776
  registerAllProviders(diffProviderRegistry);
65555
65777
  const canonicalizeProperties = makeCanonicalizePropertiesFn(diffProviderRegistry);
65778
+ const previewOrphanAdoption = async (state, effectiveTemplate, orphanStackName, orphanRegion) => {
65779
+ const outcome = await planOrphanAdoption({
65780
+ records: state.orphans ?? [],
65781
+ managedLogicalIds: new Set(Object.keys(state.resources)),
65782
+ template: effectiveTemplate,
65783
+ stackName: orphanStackName,
65784
+ region: orphanRegion,
65785
+ getProvider: (resourceType, provisionedBy) => diffProviderRegistry.getProviderFor({
65786
+ resourceType,
65787
+ provisionedBy
65788
+ }).provider,
65789
+ nameProperties: (resourceType) => {
65790
+ const property = explicitNamePropertyFor(resourceType);
65791
+ return property ? [property] : [];
65792
+ },
65793
+ readSiblingClaims: makeSiblingClaimReader({
65794
+ stateBackend,
65795
+ selfStackName: orphanStackName,
65796
+ selfRegion: orphanRegion,
65797
+ logger
65798
+ }),
65799
+ logger
65800
+ });
65801
+ return {
65802
+ adopted: outcome.adopted,
65803
+ refusals: outcome.refusals
65804
+ };
65805
+ };
65556
65806
  const recursive = options.recursive ?? false;
65557
65807
  const resolveAssetRedirect = createAssetRedirectResolver({
65558
65808
  stateBackend,
@@ -65581,17 +65831,20 @@ async function diffCommand(stacks, options) {
65581
65831
  diffCalculator,
65582
65832
  canonicalizeProperties,
65583
65833
  ...assetRedirect && { assetRedirect },
65584
- ...options.cfnFallback === false && { cfnFallback: false }
65834
+ ...options.cfnFallback === false && { cfnFallback: false },
65835
+ previewOrphanAdoption
65585
65836
  }));
65586
65837
  }
65587
65838
  if (options.json) process.stdout.write(`${JSON.stringify(trees.map(diffTreeToJson), null, 2)}\n`);
65588
65839
  else for (const tree of trees) {
65589
- if (!treeHasChanges(tree)) {
65840
+ if (!treeIsWorthRendering(tree)) {
65590
65841
  logger.info(`\n✓ No changes detected for stack ${tree.stackName}`);
65591
65842
  continue;
65592
65843
  }
65593
65844
  renderDiffTree(tree, true, (msg) => logger.info(msg));
65594
65845
  }
65846
+ const blockingCount = trees.reduce((n, tree) => n + countBlocking(tree), 0);
65847
+ if (blockingCount > 0) throw new DeployRefusalPreviewError(blockingCount);
65595
65848
  if (options.fail && trees.some(treeHasChanges)) throw new DiffDetectedError();
65596
65849
  } finally {
65597
65850
  awsClients.destroy();
@@ -66109,10 +66362,9 @@ async function scrubCommand(stacks, options) {
66109
66362
  * (issue #2005) — the outputs' own map plus every resource's, filtered to the
66110
66363
  * values long enough to be a safe needle.
66111
66364
  *
66112
- * Used ONLY by {@link redactUnaccountedOutputs}, which is why the union is built
66113
- * here rather than kept as the pass's input everywhere: `outputSecrets` and
66114
- * `perResourceSecrets` are deliberately SEPARATE bags so one resource's secret
66115
- * value cannot rewrite another resource's coinciding literal (the collision the
66365
+ * Built here rather than kept as each pass's input everywhere `outputSecrets`
66366
+ * and `perResourceSecrets` are deliberately SEPARATE bags so one resource's
66367
+ * secret value cannot rewrite another's coinciding literal (the collision the
66116
66368
  * deploy engine's `perResourceSecrets` doc describes), and widening the bag the
66117
66369
  * RESOURCE walk uses would re-open exactly that.
66118
66370
  *
@@ -66198,9 +66450,10 @@ async function scrubCommand(stacks, options) {
66198
66450
  * command can. Refusing the substring arm here would trade the irreversible
66199
66451
  * failure for the reversible one in the wrong direction.
66200
66452
  */
66201
- function allRecordedSecrets(outputSecrets, perResourceSecrets) {
66453
+ function allRecordedSecrets(outputSecrets, perResourceSecrets, orphanSecrets) {
66202
66454
  const union = /* @__PURE__ */ new Map();
66203
66455
  for (const recorded of perResourceSecrets.values()) for (const [value, expression] of recorded) union.set(value, expression);
66456
+ for (const recorded of orphanSecrets.values()) for (const [value, expression] of recorded) union.set(value, expression);
66204
66457
  for (const [value, expression] of outputSecrets) union.set(value, expression);
66205
66458
  for (const value of union.keys()) if (value.length < 4) union.delete(value);
66206
66459
  return union;
@@ -67721,6 +67974,13 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
67721
67974
  if (acquired) await lockManager.acquireLockWithRetry(stack.stackName, region, void 0, "scrub");
67722
67975
  const perResourceSecrets = /* @__PURE__ */ new Map();
67723
67976
  const outputSecrets = /* @__PURE__ */ new Map();
67977
+ /**
67978
+ * Needles derived from each rollback-orphan record's OWN recorded bag
67979
+ * (issue go-to-k/cdkd#2943), keyed by logical id — per-record for the same
67980
+ * reason `perResourceSecrets` is per-resource: one record's secret must not
67981
+ * rewrite another's coinciding literal.
67982
+ */
67983
+ const orphanSecrets = /* @__PURE__ */ new Map();
67724
67984
  const prePassFindings = { unverifiable: [] };
67725
67985
  try {
67726
67986
  const loaded = await stateBackend.getState(stack.stackName, region);
@@ -67801,6 +68061,27 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
67801
68061
  }
67802
68062
  perResourceTemplateProps.set(logicalId, templateResource.Properties);
67803
68063
  }
68064
+ for (const record of state.orphans ?? []) {
68065
+ const recordedSecretValues = /* @__PURE__ */ new Map();
68066
+ orphanSecrets.set(record.logicalId, recordedSecretValues);
68067
+ const resolveInput = await pinCrossRegionSecrets({
68068
+ properties: record.state.properties,
68069
+ attributes: record.state.attributes ?? {},
68070
+ observedProperties: record.state.observedProperties ?? {}
68071
+ }, stack.stackName, {
68072
+ stackRegion: region,
68073
+ producerRegions,
68074
+ resolvers,
68075
+ recordedSecretValues,
68076
+ origin: `orphan record '${record.logicalId}'`
68077
+ });
68078
+ try {
68079
+ await resolver.resolve(resolveInput, resolverContext(recordedSecretValues));
68080
+ } catch (err) {
68081
+ if (isRegionAmbiguousRefusal(err)) throw err;
68082
+ logger.debug(`Resolution of orphan record ${record.logicalId} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), recordedSecretValues)}`);
68083
+ }
68084
+ }
67804
68085
  for (const [name, output] of Object.entries(templateOutputs)) {
67805
68086
  const declaredExportName = output.Export?.Name;
67806
68087
  let exportName = declaredExportName;
@@ -67871,7 +68152,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
67871
68152
  secretBearingKeys.push(key);
67872
68153
  logger.warn(secretBearingStateKeyWarning(stack.stackName, display));
67873
68154
  }
67874
- const totalSecrets = outputSecrets.size + [...perResourceSecrets.values()].reduce((n, m) => n + m.size, 0);
68155
+ const totalSecrets = outputSecrets.size + [...perResourceSecrets.values()].reduce((n, m) => n + m.size, 0) + [...orphanSecrets.values()].reduce((n, m) => n + m.size, 0);
67875
68156
  if (totalSecrets === 0) return {
67876
68157
  recordsChanged: 0,
67877
68158
  secretsFound: 0,
@@ -67894,13 +68175,27 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
67894
68175
  if (JSON.stringify(scrubbed) !== JSON.stringify(record)) recordsChanged++;
67895
68176
  newResources[logicalId] = scrubbed;
67896
68177
  }
67897
- const newOutputs = redactUnaccountedOutputs(outputSecrets.size > 0 ? redactSecretsForState(state.outputs, outputSecrets, outputsSourceUntrusted ? void 0 : outputsTemplateSource, TEMPLATE_SOURCED_RULES) : state.outputs, state.outputs, accountedOutputKeys, allRecordedSecrets(outputSecrets, perResourceSecrets));
68178
+ const newOrphans = (state.orphans ?? []).map((record) => {
68179
+ const own = orphanSecrets.get(record.logicalId);
68180
+ const needles = new Map(allRecordedSecrets(outputSecrets, perResourceSecrets, orphanSecrets));
68181
+ for (const [plaintext, expression] of own ?? []) needles.set(plaintext, expression);
68182
+ if (needles.size === 0) return record;
68183
+ const scrubbed = scrubResourceRecord(record.state, needles, void 0, STATE_SOURCED_CROSS_GENERATION_RULES);
68184
+ if (JSON.stringify(scrubbed) === JSON.stringify(record.state)) return record;
68185
+ recordsChanged++;
68186
+ return {
68187
+ ...record,
68188
+ state: scrubbed
68189
+ };
68190
+ });
68191
+ const newOutputs = redactUnaccountedOutputs(outputSecrets.size > 0 ? redactSecretsForState(state.outputs, outputSecrets, outputsSourceUntrusted ? void 0 : outputsTemplateSource, TEMPLATE_SOURCED_RULES) : state.outputs, state.outputs, accountedOutputKeys, allRecordedSecrets(outputSecrets, perResourceSecrets, orphanSecrets));
67898
68192
  if (JSON.stringify(newOutputs) !== JSON.stringify(state.outputs)) recordsChanged++;
67899
68193
  if (recordsChanged > 0 && !opts.dryRun) {
67900
68194
  const { skippedOutputs: _droppedByScrub, ...carriedState } = state;
67901
68195
  const nextState = {
67902
68196
  ...carriedState,
67903
68197
  resources: newResources,
68198
+ ...state.orphans !== void 0 && { orphans: newOrphans },
67904
68199
  outputs: newOutputs,
67905
68200
  lastModified: Date.now()
67906
68201
  };
@@ -67915,7 +68210,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
67915
68210
  exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
67916
68211
  };
67917
68212
  } catch (err) {
67918
- throw maskSecretsInError(err, allRecordedSecrets(outputSecrets, perResourceSecrets));
68213
+ throw maskSecretsInError(err, allRecordedSecrets(outputSecrets, perResourceSecrets, orphanSecrets));
67919
68214
  } finally {
67920
68215
  if (acquired) await lockManager.releaseLock(stack.stackName, region).catch((err) => {
67921
68216
  logger.warn(`Failed to release lock for ${stack.stackName}: ${err instanceof Error ? err.message : String(err)}`);
@@ -68826,6 +69121,17 @@ function stripWalk(value) {
68826
69121
  //#endregion
68827
69122
  //#region src/cli/commands/drift.ts
68828
69123
  /**
69124
+ * The schema version at which `ResourceState.observedBaselineRefused` arrived.
69125
+ * A record BELOW it could not carry a refusal, which is what
69126
+ * {@link warnIfPreV10BaselineGap} keys on (issue
69127
+ * [#3011](https://github.com/go-to-k/cdkd/issues/3011)).
69128
+ *
69129
+ * A literal rather than `STATE_SCHEMA_VERSION_CURRENT`: this is a FIXED
69130
+ * historical boundary, and binding it to "current" would silently stop warning
69131
+ * about v10 records on the next bump — the population it names does not move.
69132
+ */
69133
+ const PRE_V10_BASELINE_WARNING_FLOOR = 10;
69134
+ /**
68829
69135
  * Route one outcome to the handler for its variant.
68830
69136
  *
68831
69137
  * The mechanism issue [#2135](https://github.com/go-to-k/cdkd/issues/2135) asks
@@ -69047,6 +69353,10 @@ const UNCOMPARED_REASONS = {
69047
69353
  kind: "unknown",
69048
69354
  phrase: "not compared AT ALL: the read or comparison failed"
69049
69355
  },
69356
+ baselineRefused: {
69357
+ kind: "unknown",
69358
+ phrase: "not compared AT ALL: a `cdkd import` run refused to capture their observed baseline"
69359
+ },
69050
69360
  refused: {
69051
69361
  kind: "unknown",
69052
69362
  phrase: "only PARTIALLY compared: cdkd refused to resolve a dynamic reference their state records"
@@ -69592,7 +69902,8 @@ function notComparedReason(cause) {
69592
69902
  return {
69593
69903
  refused: "cdkd refused to resolve a dynamic reference its state records (spell the reference as a full ARN, which names its region)",
69594
69904
  unresolvedToken: "its state records a `{{resolve:...}}` spelling cdkd resolves for nobody (permanent; a re-run cannot clear it)",
69595
- readFailed: "the read or comparison threw, so NONE of its properties were compared"
69905
+ readFailed: "the read or comparison threw, so NONE of its properties were compared",
69906
+ baselineRefused: "a `cdkd import` run refused to capture its observed baseline, so the only baseline available is the recorded properties that refusal already found untrustworthy (deploy a change to this resource to restore one)"
69596
69907
  }[cause];
69597
69908
  }
69598
69909
  /**
@@ -69980,6 +70291,15 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
69980
70291
  });
69981
70292
  continue;
69982
70293
  }
70294
+ if (resource.observedBaselineRefused === true) {
70295
+ outcomes.push({
70296
+ kind: "notCompared",
70297
+ logicalId,
70298
+ resourceType: resource.resourceType,
70299
+ notComparedCause: "baselineRefused"
70300
+ });
70301
+ continue;
70302
+ }
69983
70303
  try {
69984
70304
  let aws;
69985
70305
  if (provider.readCurrentState) aws = await provider.readCurrentState(resource.physicalId, logicalId, resource.resourceType, resource.properties ?? {}, buildReadCurrentStateContext(state, logicalId));
@@ -70106,17 +70426,56 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
70106
70426
  continue;
70107
70427
  }
70108
70428
  }
70429
+ const warnings = warnIfPreV10BaselineGap(state, outcomes, logger);
70109
70430
  return {
70110
70431
  stackName,
70111
70432
  region,
70112
70433
  outcomes,
70113
70434
  state,
70114
70435
  etag: result.etag,
70115
- migrationPending: result.migrationPending ?? false
70436
+ migrationPending: result.migrationPending ?? false,
70437
+ warnings
70116
70438
  };
70117
70439
  });
70118
70440
  }
70119
70441
  /**
70442
+ * Warn once for a stack whose state PREDATES the refused-baseline marker
70443
+ * (`ResourceState.observedBaselineRefused`, state schema v10) and still has a
70444
+ * resource without a drift baseline (issue
70445
+ * [#3011](https://github.com/go-to-k/cdkd/issues/3011)).
70446
+ *
70447
+ * Both conjuncts are load-bearing, and neither is a heuristic:
70448
+ *
70449
+ * - `version < 10` is not "probably imported" — it is the exact statement that
70450
+ * this record could not carry a refusal even if one was made. A v10 record
70451
+ * that omits the field is a record nothing refused, and warning there would
70452
+ * be false.
70453
+ * - the missing baseline is what makes the warning ACTIONABLE. A pre-v10 stack
70454
+ * where every resource has a baseline has nothing for drift to fall back to
70455
+ * `properties` for, so it cannot hit the disclosure.
70456
+ *
70457
+ * Exported for unit testing -- internal to the drift flow otherwise.
70458
+ */
70459
+ function warnIfPreV10BaselineGap(state, outcomes, logger) {
70460
+ if ((state.version ?? 1) >= PRE_V10_BASELINE_WARNING_FLOOR) return [];
70461
+ const compared = new Set(outcomes.flatMap((o) => matchOutcome(o, {
70462
+ drifted: (d) => [d.logicalId],
70463
+ clean: (c) => [c.logicalId],
70464
+ notCompared: (n) => [n.logicalId],
70465
+ unsupported: () => [],
70466
+ skipped: () => []
70467
+ })));
70468
+ const affected = Object.entries(state.resources ?? {}).filter(([id, r]) => r.observedProperties === void 0 && compared.has(id)).map(([id]) => id);
70469
+ if (affected.length === 0) return [];
70470
+ const shown = affected.slice(0, 10).join(", ");
70471
+ const rest = affected.length > 10 ? `, and ${affected.length - 10} more` : "";
70472
+ const finding = `${state.stackName} (${state.region ?? "unknown region"}): this stack's state predates cdkd's refused-baseline marker (schema v${state.version ?? 1}; the marker arrived in v${PRE_V10_BASELINE_WARNING_FLOOR}), and ${affected.length} of its compared resource(s) have no drift baseline: ${shown}${rest}. If any were adopted by 'cdkd import' and that import REFUSED to capture a baseline, cdkd cannot tell — so it compares them against their recorded properties, which can hold a placeholder the deployed stack never used and can surface a live secret in this report. Re-run 'cdkd import' for this stack to put the record right.`;
70473
+ const note = " Note: ANY write to this stack's state — including this run's --accept / --revert, or a deploy that changes nothing about the resources above — re-stamps it at the current schema version and SILENCES this warning without fixing those records. Only a re-import, or a deploy that actually CHANGES a listed resource, repairs one.";
70474
+ logger.warn(finding);
70475
+ logger.warn(note);
70476
+ return [finding, note];
70477
+ }
70478
+ /**
70120
70479
  * Set a value at a dotted path inside a plain object, creating intermediate
70121
70480
  * objects as needed. Mirrors `lodash.set` for the subset of paths the drift
70122
70481
  * comparator actually emits — dotted nested keys, no array indices.
@@ -70226,6 +70585,10 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
70226
70585
  for (const outcome of driftedOutcomes) {
70227
70586
  const existing = resources[outcome.logicalId];
70228
70587
  if (!existing) continue;
70588
+ if (existing.observedBaselineRefused === true) {
70589
+ logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): NOT accepted — a 'cdkd import' run refused to capture this resource's observed-properties baseline, because its recorded properties can no longer position the secret redaction. Accepting would write the AWS readback into those properties, which can persist a resolved secret into state.json in plaintext. Deploy a change to this resource to restore a baseline first.`);
70590
+ continue;
70591
+ }
70229
70592
  const hasObserved = existing.observedProperties !== void 0;
70230
70593
  const baselineSource = hasObserved ? existing.observedProperties : existing.properties ?? {};
70231
70594
  const newBaseline = JSON.parse(JSON.stringify(baselineSource));
@@ -71389,6 +71752,11 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
71389
71752
  logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): resource missing from state; skipped.`);
71390
71753
  return;
71391
71754
  }
71755
+ if (stateResource.observedBaselineRefused === true) {
71756
+ totalUnresolvable++;
71757
+ logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): NOT reverted — a 'cdkd import' run refused to capture this resource's observed-properties baseline, so the only baseline available is its recorded properties, which the refusal already found untrustworthy. Reverting from them could overwrite a live value (a resolved secret among them) with a placeholder the deployed stack never used. Deploy a change to this resource to restore a baseline first.`);
71758
+ return;
71759
+ }
71392
71760
  const provider = providerRegistry.getProviderFor({
71393
71761
  resourceType: outcome.resourceType,
71394
71762
  provisionedBy: stateResource.provisionedBy
@@ -71571,6 +71939,10 @@ function printAcceptPlan(reports, out) {
71571
71939
  const lines = [];
71572
71940
  let plannedWrites = 0;
71573
71941
  for (const o of drifted) {
71942
+ if (report.state.resources[o.logicalId]?.observedBaselineRefused === true) {
71943
+ lines.push(` ~ ${o.logicalId} (${o.resourceType})\n SKIPPED — a 'cdkd import' run refused this resource's observed-properties baseline; accepting would write the AWS readback into properties it already found untrustworthy. Deploy a change to this resource first.\n`);
71944
+ continue;
71945
+ }
71574
71946
  lines.push(` ~ ${o.logicalId} (${o.resourceType})\n`);
71575
71947
  for (const change of o.changes) {
71576
71948
  const refusal = acceptRefusalReason(change, o.maskedPaths);
@@ -71603,6 +71975,10 @@ function printRevertPlan(reports, out) {
71603
71975
  if (drifted.length === 0) continue;
71604
71976
  out.write(`\nPlan (--revert): push cdkd state values back into AWS for ${report.stackName} (${report.region}):\n`);
71605
71977
  for (const o of drifted) {
71978
+ if (report.state.resources[o.logicalId]?.observedBaselineRefused === true) {
71979
+ out.write(` ! ${o.logicalId} (${o.resourceType}): NOT reverted — a 'cdkd import' run refused this resource's observed-properties baseline, so the only baseline available is the one that refusal already found untrustworthy. Deploy a change to this resource first.\n`);
71980
+ continue;
71981
+ }
71606
71982
  const word = o.changes.length === 1 ? "property path" : "property paths";
71607
71983
  out.write(` → provider.update on ${o.logicalId} (${o.resourceType}): revert ${o.changes.length} ${word}\n`);
71608
71984
  for (const change of o.changes) out.write(` ${change.path}: ${formatScalar(change.awsValue)} -> ${formatScalar(change.stateValue)}\n`);
@@ -71709,12 +72085,13 @@ function writeJsonReport(reports) {
71709
72085
  const notCompared = notComparedOutcomes(r).map(({ outcome, cause }) => ({
71710
72086
  logicalId: outcome.logicalId,
71711
72087
  type: outcome.resourceType,
71712
- referencesUnresolved: cause !== "readFailed",
72088
+ referencesUnresolved: cause !== "readFailed" && cause !== "baselineRefused",
71713
72089
  cause
71714
72090
  }));
71715
72091
  return {
71716
72092
  stack: r.stackName,
71717
72093
  region: r.region,
72094
+ ...r.warnings.length > 0 && { warnings: r.warnings },
71718
72095
  drifted,
71719
72096
  clean,
71720
72097
  notSupported,
@@ -71753,7 +72130,7 @@ function writeHumanReport(reports) {
71753
72130
  const checked = inspected - notCompared.length;
71754
72131
  if (drifted.length === 0) {
71755
72132
  if (notCompared.length === 0 && checked === 0 && report.outcomes.length > 0) process.stdout.write(`⚠ ${report.stackName} (${report.region}): no drift detected, but NOTHING was compared — 0 of ${report.outcomes.length} resource${report.outcomes.length === 1 ? "" : "s"} checked (${unsupported.length} unsupported, ${skippedCount} skipped)\n`);
71756
- else if (notCompared.length > 0) process.stdout.write(`⚠ ${report.stackName} (${report.region}): no drift detected, but ${checked} of ${inspected} resource${inspected === 1 ? "" : "s"} fully checked ` + (notCompared.some((n) => n.cause === "readFailed") ? `(${notCompared.length} not fully compared), ` : `(${notCompared.length} only partially compared), `) + `${unsupported.length} unsupported\n`);
72133
+ else if (notCompared.length > 0) process.stdout.write(`⚠ ${report.stackName} (${report.region}): no drift detected, but ${checked} of ${inspected} resource${inspected === 1 ? "" : "s"} fully checked ` + (notCompared.some((n) => n.cause === "readFailed" || n.cause === "baselineRefused") ? `(${notCompared.length} not fully compared), ` : `(${notCompared.length} only partially compared), `) + `${unsupported.length} unsupported\n`);
71757
72134
  else process.stdout.write(`✓ ${report.stackName} (${report.region}): no drift detected (${checked} resource${checked === 1 ? "" : "s"} checked, ${unsupported.length} unsupported)\n`);
71758
72135
  } else {
71759
72136
  const word = drifted.length === 1 ? "resource" : "resources";
@@ -71768,9 +72145,12 @@ function writeHumanReport(reports) {
71768
72145
  }
71769
72146
  }
71770
72147
  if (notCompared.length > 0) {
71771
- const readFailed = notCompared.filter((n) => n.cause === "readFailed").length;
71772
- const referenceCaused = notCompared.length - readFailed;
71773
- process.stdout.write(readFailed === 0 ? `\n ${notCompared.length} resource(s) only PARTIALLY compared — cdkd could not, or refused to, resolve a dynamic reference their state records, so their secret-bearing properties were NOT compared:\n` : `\n ${notCompared.length} resource(s) NOT fully compared — ${readFailed} not compared AT ALL (the read or comparison failed)` + (referenceCaused > 0 ? `, ${referenceCaused} only PARTIALLY compared (a dynamic reference cdkd could not, or refused to, resolve)` : "") + `:\n`);
72148
+ const notComparedAtAll = notCompared.filter((n) => n.cause === "readFailed" || n.cause === "baselineRefused").length;
72149
+ const readFailedCount = notCompared.filter((n) => n.cause === "readFailed").length;
72150
+ const baselineRefusedCount = notCompared.filter((n) => n.cause === "baselineRefused").length;
72151
+ const referenceCaused = notCompared.length - notComparedAtAll;
72152
+ const atAllCause = [...readFailedCount > 0 ? ["the read or comparison failed"] : [], ...baselineRefusedCount > 0 ? ["an import refused their observed baseline"] : []].join("; ");
72153
+ process.stdout.write(notComparedAtAll === 0 ? `\n ${notCompared.length} resource(s) only PARTIALLY compared — cdkd could not, or refused to, resolve a dynamic reference their state records, so their secret-bearing properties were NOT compared:\n` : `\n ${notCompared.length} resource(s) NOT fully compared — ${notComparedAtAll} not compared AT ALL (${atAllCause})` + (referenceCaused > 0 ? `, ${referenceCaused} only PARTIALLY compared (a dynamic reference cdkd could not, or refused to, resolve)` : "") + `:\n`);
71774
72154
  for (const { outcome, cause } of notCompared) process.stdout.write(` ! ${outcome.logicalId} (${outcome.resourceType}) — ${notComparedReason(cause)}\n`);
71775
72155
  }
71776
72156
  if (unsupported.length > 0) {
@@ -75015,9 +75395,18 @@ function reportDriftBaselineGaps(state, logger) {
75015
75395
  logger.warn(`cdkd state schema is v${state.version} (pre-observedProperties). cdkd drift cannot reliably compare against AWS for this stack; the next \`cdk deploy\` after migration may surface spurious changes if AWS has drifted from the template. Run \`cdkd state refresh-observed ${state.stackName}\` (or any redeploy) before export to capture an AWS-current baseline.`);
75016
75396
  return;
75017
75397
  }
75018
- logger.warn(`${missing.length} of ${entries.length} resource(s) in cdkd state lack an AWS-current baseline (observedProperties). cdkd drift may produce false positives for them; the next \`cdk deploy\` after migration may surface unexpected changes. Run \`cdkd state refresh-observed ${state.stackName}\` to capture a baseline before export, then \`cdkd drift\` to verify the stack matches AWS.`);
75019
- for (const [logicalId] of missing.slice(0, 10)) logger.warn(` ${logicalId}`);
75020
- if (missing.length > 10) logger.warn(` ... and ${missing.length - 10} more`);
75398
+ const refused = missing.filter(([, r]) => r.observedBaselineRefused === true);
75399
+ const refreshable = missing.filter(([, r]) => r.observedBaselineRefused !== true);
75400
+ if (refreshable.length > 0) {
75401
+ logger.warn(`${refreshable.length} of ${entries.length} resource(s) in cdkd state lack an AWS-current baseline (observedProperties). cdkd drift may produce false positives for them; the next \`cdk deploy\` after migration may surface unexpected changes. Run \`cdkd state refresh-observed ${state.stackName}\` to capture a baseline before export, then \`cdkd drift\` to verify the stack matches AWS.`);
75402
+ for (const [logicalId] of refreshable.slice(0, 10)) logger.warn(` ${logicalId}`);
75403
+ if (refreshable.length > 10) logger.warn(` ... and ${refreshable.length - 10} more`);
75404
+ }
75405
+ if (refused.length > 0) {
75406
+ logger.warn(`${refused.length} of ${entries.length} resource(s) had their baseline REFUSED by a \`cdkd import\` run, because their recorded properties can no longer position the secret redaction. \`cdkd state refresh-observed\` will decline them too — capturing a readback against those properties could persist a resolved secret into state.json in plaintext. Deploy a change to each one to restore its baseline.`);
75407
+ for (const [logicalId] of refused.slice(0, 10)) logger.warn(` ${logicalId}`);
75408
+ if (refused.length > 10) logger.warn(` ... and ${refused.length - 10} more`);
75409
+ }
75021
75410
  }
75022
75411
  /**
75023
75412
  * Walk every stack other than `exportingStackName` looking for
@@ -76374,10 +76763,10 @@ async function stateResourcesCommand(stackName, options) {
76374
76763
  if (options.long) {
76375
76764
  const lines = [];
76376
76765
  for (const detail of details) {
76377
- lines.push(detail.logicalId);
76378
- lines.push(` Type: ${detail.resourceType}`);
76379
- lines.push(` PhysicalID: ${stripControlChars(detail.physicalId)}`);
76380
- lines.push(` Dependencies: ${detail.dependencies.length > 0 ? detail.dependencies.join(", ") : "(none)"}`);
76766
+ lines.push(stripControlChars(detail.logicalId));
76767
+ lines.push(` Type: ${formatAttributeValue(detail.resourceType)}`);
76768
+ lines.push(` PhysicalID: ${formatAttributeValue(detail.physicalId)}`);
76769
+ lines.push(` Dependencies: ${formatDependencyList(detail.dependencies)}`);
76381
76770
  const attrEntries = Object.entries(detail.attributes);
76382
76771
  if (attrEntries.length === 0) lines.push(" Attributes: (none)");
76383
76772
  else {
@@ -76390,15 +76779,27 @@ async function stateResourcesCommand(stackName, options) {
76390
76779
  process.stdout.write(`${lines.join("\n")}\n`);
76391
76780
  return;
76392
76781
  }
76393
- const idWidth = Math.max(...details.map((d) => d.logicalId.length));
76394
- const typeWidth = Math.max(...details.map((d) => d.resourceType.length));
76395
- for (const detail of details) process.stdout.write(`${detail.logicalId.padEnd(idWidth)} ${detail.resourceType.padEnd(typeWidth)} ${detail.physicalId}\n`);
76782
+ const rows = details.map((d) => ({
76783
+ logicalId: stripControlChars(d.logicalId),
76784
+ resourceType: formatAttributeValue(d.resourceType),
76785
+ physicalId: formatAttributeValue(d.physicalId)
76786
+ }));
76787
+ const idWidth = Math.max(...rows.map((r) => r.logicalId.length));
76788
+ const typeWidth = Math.max(...rows.map((r) => r.resourceType.length));
76789
+ for (const row of rows) process.stdout.write(`${row.logicalId.padEnd(idWidth)} ${row.resourceType.padEnd(typeWidth)} ${row.physicalId}\n`);
76396
76790
  } finally {
76397
76791
  setup.dispose();
76398
76792
  }
76399
76793
  }
76400
76794
  /**
76401
- * Render a single attribute value for the `--long` human-readable form.
76795
+ * Render one record-derived value into a human row.
76796
+ *
76797
+ * Named for its first caller and used well beyond an attribute now: a state or
76798
+ * lock record is read as an unchecked cast, so a field declared `string` can
76799
+ * hold anything, and the bare strip throws on it and empties the whole render.
76800
+ * This survives that, and for an actual string it is byte-identical to the
76801
+ * strip — which is why it is the default choice for a record FIELD, and the bare
76802
+ * strip is kept only where the value is a string by construction.
76402
76803
  *
76403
76804
  * Scalar values render as-is; objects/arrays are JSON-encoded inline so a
76404
76805
  * resource block stays compact even when an attribute is structured.
@@ -76417,16 +76818,24 @@ async function stateResourcesCommand(stackName, options) {
76417
76818
  * pretty-prints — `JSON.stringify` is called without an indent, so a newline
76418
76819
  * here is content injected by the value, not layout.
76419
76820
  *
76420
- * Only the human path routes through this; `--json` serializes the state
76421
- * record directly and stays byte-faithful, which is the same split
76422
- * `renderOutputChangeLines` makes.
76821
+ * Only the human path routes through this, which is the same split
76822
+ * `renderOutputChangeLines` makes. `--json` is the OTHER side of it rather than
76823
+ * a byte-faithful echo: `state show --json` reserializes the parsed record and
76824
+ * the lock it prints was sanitised upstream, and `state resources --json` emits
76825
+ * a constructed summary with defaults filled in. What `--json` does not do is
76826
+ * pass a value through THIS function.
76423
76827
  */
76424
76828
  function formatAttributeValue(value) {
76425
76829
  if (value === null) return "null";
76426
76830
  if (value === void 0) return "undefined";
76427
76831
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return stripControlChars(String(value));
76428
- const json = JSON.stringify(value);
76429
- return json === void 0 ? "(unserializable)" : stripControlChars(json);
76832
+ let json;
76833
+ try {
76834
+ json = JSON.stringify(value);
76835
+ } catch {
76836
+ json = void 0;
76837
+ }
76838
+ return json === void 0 ? UNSERIALIZABLE : stripControlChars(json);
76430
76839
  }
76431
76840
  /**
76432
76841
  * Render a duration in milliseconds as `1m23s` / `45s`.
@@ -76437,7 +76846,65 @@ function formatDuration(ms) {
76437
76846
  return `${Math.floor(seconds / 60)}m${seconds % 60}s`;
76438
76847
  }
76439
76848
  /**
76849
+ * Render a whole `Dependencies` row from a list that is an unchecked cast.
76850
+ *
76851
+ * It owns the `(none)` case as well as the values, so that no caller touches
76852
+ * `.length` on a value whose type it cannot trust — reading `.length` is itself
76853
+ * a throw site, and `"A".length > 0` would pass an emptiness test and then
76854
+ * throw inside `join`, which is a method of an ARRAY. So the test is
76855
+ * array-ness. Elements are rendered ONE AT A TIME through the field guard
76856
+ * rather than joined by it: an element that throws on STRING COERCION
76857
+ * (`{"toString": null}` is valid JSON, and `JSON.stringify` handles it fine)
76858
+ * throws INSIDE `join`, where no guard wrapping the result can catch it.
76859
+ *
76860
+ * ABSENT and `null` part company here, deliberately. An absent field is a
76861
+ * record saying nothing, which `(none)` states correctly; a `null` is a record
76862
+ * saying `null`, and printing `(none)` over it would claim something the record
76863
+ * does not. JSON can express `null` here and a hand-edited record can carry it,
76864
+ * so the difference is worth the surprise of seeing `Dependencies: null`.
76865
+ */
76866
+ function formatDependencyList(deps) {
76867
+ if (Array.isArray(deps)) return deps.length > 0 ? deps.map((dep) => formatAttributeValue(dep)).join(", ") : "(none)";
76868
+ return deps === void 0 ? "(none)" : formatAttributeValue(deps);
76869
+ }
76870
+ /**
76871
+ * Render `Last Modified` from a record whose `lastModified` is an unchecked cast.
76872
+ *
76873
+ * `new Date(x).toISOString()` throws a RangeError on a value it cannot date —
76874
+ * an out-of-range number, most non-numeric strings — and that empties the whole
76875
+ * render, the failure `formatAttributeValue` exists to stop everywhere else in
76876
+ * this block. `new Date` ITSELF throws a TypeError on an object whose
76877
+ * `toString` is unusable (`{"toString": null}` is valid JSON), which is why the
76878
+ * construction is inside the `try` and not only the formatting.
76879
+ *
76880
+ * Only the throw is guarded: the value still reaches `new Date` as-is, so every
76881
+ * input the previous line already dated renders the same ISO string — an ISO or
76882
+ * `YYYY-MM-DD` string, `null` and a boolean included. Anything else falls
76883
+ * through to the guard the other fields take.
76884
+ */
76885
+ function formatLastModified(value) {
76886
+ try {
76887
+ return new Date(value).toISOString();
76888
+ } catch {
76889
+ return formatAttributeValue(value);
76890
+ }
76891
+ }
76892
+ /**
76440
76893
  * Render lock metadata for the `state show` block.
76894
+ *
76895
+ * This row takes NO display guard — `Version` is the other, for its own reason —
76896
+ * and here the reason is upstream: `LockManager.getLockRecord` already passes
76897
+ * `owner` and `operation` through `displaySafe`, which coerces and replaces the
76898
+ * whole control class with spaces. They arrive here as control-free strings, so
76899
+ * a guard would be redundant — and a test for one could only be written by
76900
+ * mocking the read that sanitises them, which would pin nothing about the real
76901
+ * path. `displaySafe`'s own `String(value)` is unguarded, so a value that throws
76902
+ * on coercion fails THERE, before this function; that is go-to-k/cdkd#2947 and
76903
+ * not something a guard here could catch either.
76904
+ *
76905
+ * `expiresAt` is declared a number and is not guaranteed to be one, but it
76906
+ * reaches the row only through subtraction and `formatDuration`, so no character
76907
+ * the record carries can survive into the output either.
76441
76908
  */
76442
76909
  function formatLockSummary(lockInfo) {
76443
76910
  if (!lockInfo) return "unlocked";
@@ -76459,8 +76926,8 @@ function createStateResourcesCommand() {
76459
76926
  * `cdkd state show <stack>` command implementation
76460
76927
  *
76461
76928
  * Renders the full state record for one stack: stack-level metadata, lock
76462
- * status, outputs, and every resource (including properties). The deepest /
76463
- * most verbose `state` subcommand — use `state list` / `state resources` for
76929
+ * status, outputs, skipped outputs, and every resource (including properties).
76930
+ * The deepest / most verbose `state` subcommand — use `state list` / `state resources` for
76464
76931
  * lighter inspection.
76465
76932
  *
76466
76933
  * When the same stack name has state in multiple regions, requires
@@ -76473,8 +76940,9 @@ function createStateResourcesCommand() {
76473
76940
  * - `--show-nested` (issue #555 A4): when the stack contains
76474
76941
  * `AWS::CloudFormation::Stack` rows, recursively load every child state
76475
76942
  * record (`cdkd/<parent>~<childLogicalId>/<region>/state.json`) and append
76476
- * its block after the parent's. No-op when the stack has no nested
76477
- * children.
76943
+ * its block after the parent's. With no nested children the rendered
76944
+ * blocks are the same, though a `Skipped outputs:` explanation moves to
76945
+ * the end of the tree.
76478
76946
  */
76479
76947
  async function stateShowCommand(stackName, options) {
76480
76948
  const logger = getLogger();
@@ -76503,27 +76971,82 @@ async function stateShowCommand(stackName, options) {
76503
76971
  }, null, 2)}\n`);
76504
76972
  return;
76505
76973
  }
76506
- process.stdout.write(`${renderStateBlock(stateResult.state, lockInfo).join("\n")}\n`);
76974
+ process.stdout.write(`${renderStateBlock(stateResult.state, lockInfo, true).join("\n")}\n`);
76507
76975
  } finally {
76508
76976
  setup.dispose();
76509
76977
  }
76510
76978
  }
76511
76979
  /**
76980
+ * How much of a `skippedOutputs` digest the text view shows. A prefix
76981
+ * DIFFERENCE proves two records differ, which is what a reader wants at a
76982
+ * glance; a prefix MATCH proves nothing, so an exact comparison belongs to
76983
+ * `--json`, which carries the digest whole.
76984
+ */
76985
+ const SKIPPED_DIGEST_PREVIEW_LEN = 12;
76986
+ /** What `formatAttributeValue` prints for a value `JSON.stringify` refuses. */
76987
+ const UNSERIALIZABLE = "(unserializable)";
76988
+ /**
76989
+ * The `skippedOutputs` rows a state record owes, sorted by key.
76990
+ */
76991
+ function sortedSkippedOutputs(state) {
76992
+ return Object.entries(state.skippedOutputs ?? {}).sort(([a], [b]) => a.localeCompare(b));
76993
+ }
76994
+ /** Whether one record renders the block. The ONE copy of that condition. */
76995
+ function rendersSkippedBlock(state) {
76996
+ return Object.keys(state.skippedOutputs ?? {}).length > 0;
76997
+ }
76998
+ /**
76999
+ * The explanation printed under a `Skipped outputs:` block.
77000
+ *
77001
+ * At column ZERO, unlike the rows it follows. At the rows' two-space indent a
77002
+ * reader — or a `grep '^ '` — takes these lines for another key. It also
77003
+ * quotes `Outputs:` mid-sentence, so a test matching that as a SUBSTRING has
77004
+ * to anchor on the section's own line to mean anything.
77005
+ *
77006
+ * Emitted ONCE per render rather than per block: under `--show-nested` the
77007
+ * same explanation would otherwise repeat for every child that skipped
77008
+ * anything.
77009
+ *
77010
+ * That placement is why the opening sentence NAMES its referent rather than
77011
+ * saying "these". At the end of a tree render it follows every rendered stack
77012
+ * block, so it is not adjacent to the block it explains and a bare
77013
+ * demonstrative reads as belonging to whichever stack printed last. The
77014
+ * single-stack path still has the adjacency; the wording holds in both.
77015
+ *
77016
+ * It does not claim `Outputs:` is on the screen. That block is omitted when
77017
+ * the bag is empty, which includes the first-deploy case this record exists
77018
+ * for — every output failed, so `outputs` is `{}` while `skippedOutputs` is
77019
+ * populated.
77020
+ */
77021
+ function skippedOutputsLegend() {
77022
+ return [
77023
+ "",
77024
+ "The last deploy could not resolve the keys listed under `Skipped outputs:`",
77025
+ "above, and recorded a digest of their template inputs. While the record",
77026
+ "binds and a key is still absent from the stored outputs, `cdkd diff`",
77027
+ "previews it as ABSENT — no row, no warning. A key whose earlier value",
77028
+ "was retained is also stored under `Outputs:` here, where the record does",
77029
+ "NOT suppress it: the ordinary rules apply, up to `cdkd diff` suppressing",
77030
+ "its whole Outputs section if the key still cannot resolve.",
77031
+ "Binding rule: `bindingSkippedOutputs` in src/analyzer/skipped-outputs.ts."
77032
+ ];
77033
+ }
77034
+ /**
76512
77035
  * Render one stack's state record as a human-readable multi-line block —
76513
77036
  * the shared body used by both the single-stack default output and each
76514
77037
  * nested child rendered under `--show-nested`.
76515
77038
  */
76516
- function renderStateBlock(state, lockInfo) {
77039
+ function renderStateBlock(state, lockInfo, withLegend) {
76517
77040
  const lines = [];
76518
- lines.push(`Stack: ${state.stackName}`);
76519
- if (state.region) lines.push(` Region: ${state.region}`);
77041
+ lines.push(`Stack: ${formatAttributeValue(state.stackName)}`);
77042
+ if (state.region) lines.push(` Region: ${formatAttributeValue(state.region)}`);
76520
77043
  lines.push(` Version: ${state.version}`);
76521
- lines.push(` Last Modified: ${new Date(state.lastModified).toISOString()}`);
77044
+ lines.push(` Last Modified: ${formatLastModified(state.lastModified)}`);
76522
77045
  lines.push(` Lock: ${formatLockSummary(lockInfo)}`);
76523
77046
  if (state.parentStack !== void 0) {
76524
- const parentRegionStr = state.parentRegion ? ` (${state.parentRegion})` : "";
76525
- const logicalIdStr = state.parentLogicalId !== void 0 ? `, logical id: ${state.parentLogicalId}` : "";
76526
- lines.push(` Parent: ${state.parentStack}${parentRegionStr}${logicalIdStr}`);
77047
+ const parentRegionStr = state.parentRegion ? ` (${formatAttributeValue(state.parentRegion)})` : "";
77048
+ const logicalIdStr = state.parentLogicalId !== void 0 ? `, logical id: ${formatAttributeValue(state.parentLogicalId)}` : "";
77049
+ lines.push(` Parent: ${formatAttributeValue(state.parentStack)}${parentRegionStr}${logicalIdStr}`);
76527
77050
  }
76528
77051
  const outputEntries = Object.entries(state.outputs ?? {});
76529
77052
  if (outputEntries.length > 0) {
@@ -76531,18 +77054,28 @@ function renderStateBlock(state, lockInfo) {
76531
77054
  lines.push("Outputs:");
76532
77055
  for (const [k, v] of outputEntries) lines.push(` ${stripControlChars(k)}: ${formatAttributeValue(v)}`);
76533
77056
  }
77057
+ if (rendersSkippedBlock(state)) {
77058
+ lines.push("");
77059
+ lines.push("Skipped outputs:");
77060
+ for (const [k, digest] of sortedSkippedOutputs(state)) {
77061
+ const fullDigest = formatAttributeValue(digest);
77062
+ const shownDigest = fullDigest.length > SKIPPED_DIGEST_PREVIEW_LEN && fullDigest !== UNSERIALIZABLE ? `${fullDigest.slice(0, SKIPPED_DIGEST_PREVIEW_LEN)}…` : fullDigest;
77063
+ lines.push(` ${stripControlChars(k)}: ${shownDigest}`);
77064
+ }
77065
+ if (withLegend) lines.push(...skippedOutputsLegend());
77066
+ }
76534
77067
  const resourceEntries = Object.entries(state.resources ?? {}).sort(([a], [b]) => a.localeCompare(b));
76535
77068
  lines.push("");
76536
77069
  lines.push(`Resources (${resourceEntries.length}):`);
76537
77070
  for (const [logicalId, resource] of resourceEntries) {
76538
77071
  lines.push("");
76539
- lines.push(logicalId);
76540
- lines.push(` Type: ${resource.resourceType}`);
76541
- lines.push(` PhysicalID: ${stripControlChars(resource.physicalId)}`);
77072
+ lines.push(stripControlChars(logicalId));
77073
+ lines.push(` Type: ${formatAttributeValue(resource.resourceType)}`);
77074
+ lines.push(` PhysicalID: ${formatAttributeValue(resource.physicalId)}`);
76542
77075
  const provisionedBy = resource.provisionedBy ?? "(sdk, legacy default)";
76543
- lines.push(` ProvisionedBy: ${provisionedBy}`);
76544
- const deps = resource.dependencies ?? [];
76545
- lines.push(` Dependencies: ${deps.length > 0 ? deps.join(", ") : "(none)"}`);
77076
+ lines.push(` ProvisionedBy: ${formatAttributeValue(provisionedBy)}`);
77077
+ if (resource.observedBaselineRefused === true) lines.push(" ObservedBaseline: REFUSED by 'cdkd import' — no baseline will be captured (deploy a change to this resource to restore one)");
77078
+ lines.push(` Dependencies: ${formatDependencyList(resource.dependencies)}`);
76546
77079
  const attrEntries = Object.entries(resource.attributes ?? {});
76547
77080
  if (attrEntries.length === 0) lines.push(" Attributes: (none)");
76548
77081
  else {
@@ -76598,6 +77131,14 @@ async function loadLocksForTree(tree, lockManager, rootLock) {
76598
77131
  return decorate(tree);
76599
77132
  }
76600
77133
  /**
77134
+ * Whether any node in this tree owes a `Skipped outputs:` block, and so
77135
+ * whether the tree render owes the legend once at its end. Asked through
77136
+ * {@link rendersSkippedBlock}, the same predicate the block's own guard uses.
77137
+ */
77138
+ function treeOwesSkippedLegend(node) {
77139
+ return rendersSkippedBlock(node.state) || node.children.some(treeOwesSkippedLegend);
77140
+ }
77141
+ /**
76601
77142
  * Render the parent's block followed by every descendant in DFS order,
76602
77143
  * each separated by a blank line and prefixed with a `Nested stack: ...`
76603
77144
  * header so the user can scan the tree top-down in one screen.
@@ -76608,15 +77149,16 @@ async function loadLocksForTree(tree, lockManager, rootLock) {
76608
77149
  */
76609
77150
  function renderTreeWithChildren(root) {
76610
77151
  const lines = [];
76611
- lines.push(...renderStateBlock(root.state, root.lock));
77152
+ lines.push(...renderStateBlock(root.state, root.lock, false));
76612
77153
  appendDescendants(root.children, lines);
77154
+ if (treeOwesSkippedLegend(root)) lines.push(...skippedOutputsLegend());
76613
77155
  return lines;
76614
77156
  }
76615
77157
  function appendDescendants(children, out) {
76616
77158
  for (const child of children) {
76617
77159
  out.push("");
76618
- out.push(`Nested stack: ${child.stackName}`);
76619
- out.push(...renderStateBlock(child.state, child.lock));
77160
+ out.push(`Nested stack: ${stripControlChars(child.stackName)}`);
77161
+ out.push(...renderStateBlock(child.state, child.lock, false));
76620
77162
  appendDescendants(child.children, out);
76621
77163
  }
76622
77164
  }
@@ -77268,6 +77810,7 @@ async function stateRefreshObservedCommand(stackArgs, options) {
77268
77810
  let totalRefreshed = 0;
77269
77811
  let totalUnsupported = 0;
77270
77812
  let totalFailed = 0;
77813
+ let totalRefusedBaseline = 0;
77271
77814
  for (const target of targets) {
77272
77815
  if (!target.region) throw new Error(`Stack '${target.stackName}' has only a legacy state record without a region. Run 'cdkd deploy ${target.stackName}' (or any cdkd write) first to migrate it to the region-scoped layout, then re-run refresh-observed.`);
77273
77816
  const counts = await refreshObservedForStack(target.stackName, target.region, setup.stateBackend, setup.lockManager, providerRegistry, {
@@ -77282,9 +77825,11 @@ async function stateRefreshObservedCommand(stackArgs, options) {
77282
77825
  totalRefreshed += counts.refreshed;
77283
77826
  totalUnsupported += counts.unsupported;
77284
77827
  totalFailed += counts.failed;
77828
+ totalRefusedBaseline += counts.refusedBaseline;
77285
77829
  }
77286
77830
  const summary = options.dryRun ? `Plan: ${totalRefreshed} resource(s) would be refreshed, ${totalUnsupported} unsupported, ${totalFailed} would fail (--dry-run, no state was written)` : `Done: ${totalRefreshed} resource(s) refreshed, ${totalUnsupported} unsupported, ${totalFailed} failed`;
77287
77831
  logger.info(`\n${summary}`);
77832
+ if (totalRefusedBaseline > 0) logger.warn(`${totalRefusedBaseline} resource(s) ${options.dryRun ? "would NOT be refreshed" : "were NOT refreshed"}: a 'cdkd import' run refused to capture their observed-properties baseline, because their recorded properties can no longer position the secret redaction — refreshing against those properties could persist a resolved secret into state.json in plaintext. A deploy that actually CHANGES one of them restores its baseline — a NO_CHANGE deploy does not, and re-running this command will refuse them again. Drift compares against their recorded properties until then.`);
77288
77833
  if (totalFailed > 0) throw new PartialFailureError(`Refresh completed with ${totalFailed} per-resource readCurrentState failure(s). Affected resources keep their previous observedProperties (or no observedProperties at all). Re-run 'cdkd state refresh-observed' to retry.`);
77289
77834
  } finally {
77290
77835
  setup.dispose();
@@ -77351,13 +77896,19 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
77351
77896
  return {
77352
77897
  refreshed: 0,
77353
77898
  unsupported: 0,
77354
- failed: 0
77899
+ failed: 0,
77900
+ refusedBaseline: 0
77355
77901
  };
77356
77902
  }
77357
77903
  if (opts.dryRun) {
77358
77904
  let wouldRefresh = 0;
77359
77905
  let wouldUnsupported = 0;
77906
+ let wouldRefuse = 0;
77360
77907
  for (const [, resource] of entries) {
77908
+ if (resource.observedBaselineRefused === true) {
77909
+ wouldRefuse++;
77910
+ continue;
77911
+ }
77361
77912
  let provider;
77362
77913
  try {
77363
77914
  provider = providerRegistry.getProviderFor({
@@ -77371,11 +77922,12 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
77371
77922
  if (provider.readCurrentState) wouldRefresh++;
77372
77923
  else wouldUnsupported++;
77373
77924
  }
77374
- logger.info(`Plan ${stackName} (${region}): ${wouldRefresh} resource(s) would be refreshed, ${wouldUnsupported} unsupported`);
77925
+ logger.info(`Plan ${stackName} (${region}): ${wouldRefresh} resource(s) would be refreshed, ${wouldUnsupported} unsupported` + (wouldRefuse > 0 ? `, ${wouldRefuse} refused (import baseline refusal)` : ""));
77375
77926
  return {
77376
77927
  refreshed: wouldRefresh,
77377
77928
  unsupported: wouldUnsupported,
77378
- failed: 0
77929
+ failed: 0,
77930
+ refusedBaseline: wouldRefuse
77379
77931
  };
77380
77932
  }
77381
77933
  const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
@@ -77389,8 +77941,13 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
77389
77941
  let refreshed = 0;
77390
77942
  let unsupported = 0;
77391
77943
  let failed = 0;
77944
+ let refusedBaseline = 0;
77392
77945
  await withStackName(stackName, async () => {
77393
77946
  const tasks = entries.map(async ([logicalId, resource]) => {
77947
+ if (resource.observedBaselineRefused === true) {
77948
+ refusedBaseline++;
77949
+ return;
77950
+ }
77394
77951
  if (providerRegistry.shouldSkipResource(resource.resourceType)) {
77395
77952
  unsupported++;
77396
77953
  return;
@@ -77429,11 +77986,12 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
77429
77986
  const saveOptions = { expectedEtag: etag };
77430
77987
  if (migrationPending) saveOptions.migrateLegacy = true;
77431
77988
  await stateBackend.saveState(stackName, region, state, saveOptions);
77432
- logger.info(`✓ ${stackName} (${region}): ${refreshed} refreshed, ${unsupported} unsupported, ${failed} failed`);
77989
+ logger.info(`✓ ${stackName} (${region}): ${refreshed} refreshed, ${unsupported} unsupported, ${failed} failed` + (refusedBaseline > 0 ? `, ${refusedBaseline} refused (import baseline refusal)` : ""));
77433
77990
  return {
77434
77991
  refreshed,
77435
77992
  unsupported,
77436
- failed
77993
+ failed,
77994
+ refusedBaseline
77437
77995
  };
77438
77996
  } finally {
77439
77997
  await lockManager.releaseLock(stackName, region).catch((err) => {
@@ -77674,6 +78232,7 @@ async function rollbackCommand(stackArg, options) {
77674
78232
  if (!stateData) throw new Error(`Rollback journal exists for '${stackName}' (${region}) but its state.json is missing (keys: ${setup.prefix}/${stackName}/${region}/state.json and .../rollback-journal.json). State appears corrupted — inspect the bucket manually.`);
77675
78233
  const baseState = stateData.state;
77676
78234
  const stateResources = { ...baseState.resources };
78235
+ const mintedOrphans = [];
77677
78236
  const orphanLogicalIds = new Set(options.orphan ?? []);
77678
78237
  const newestSegment = journal.segments[journal.segments.length - 1];
77679
78238
  if (newestSegment.roleArn && !options.roleArn) logger.info(`Note: the failed deploy ran with --role-arn ${newestSegment.roleArn}; this rollback is running with ambient credentials (pass --role-arn to match).`);
@@ -77720,9 +78279,10 @@ async function rollbackCommand(stackArg, options) {
77720
78279
  const { skippedOutputs: _droppedByRollback, ...carriedState } = baseState;
77721
78280
  const next = () => ({
77722
78281
  ...carriedState,
77723
- version: 9,
78282
+ version: 10,
77724
78283
  region,
77725
78284
  resources: { ...stateResources },
78285
+ ...orphansAfterRollback(baseState, mintedOrphans),
77726
78286
  lastModified: Date.now()
77727
78287
  });
77728
78288
  try {
@@ -77764,7 +78324,8 @@ async function rollbackCommand(stackArg, options) {
77764
78324
  const failedResult = await replayFailedOperations(segment.failedOperations, stateResources, stackName, ctx, {
77765
78325
  afterOp: saveState,
77766
78326
  isInterrupted: () => interrupted,
77767
- emitEnvelope: segment.operations.length === 0
78327
+ emitEnvelope: segment.operations.length === 0,
78328
+ onOrphan: (record) => mintedOrphans.push(record)
77768
78329
  });
77769
78330
  failedOpFailures = failedResult.failures;
77770
78331
  failedOpWarnings = failedResult.warnings;
@@ -77785,7 +78346,8 @@ async function rollbackCommand(stackArg, options) {
77785
78346
  const replayResult = await replayRollback(segment.operations, stateResources, stackName, ctx, {
77786
78347
  orphanLogicalIds,
77787
78348
  afterOp: saveState,
77788
- isInterrupted: () => interrupted
78349
+ isInterrupted: () => interrupted,
78350
+ onOrphan: (record) => mintedOrphans.push(record)
77789
78351
  });
77790
78352
  return {
77791
78353
  failures: replayResult.failures + failedOpFailures,
@@ -77806,7 +78368,8 @@ async function rollbackCommand(stackArg, options) {
77806
78368
  } finally {
77807
78369
  await eventRecorder.finalize(totalFailures > 0 || interrupted ? "FAILED" : "SUCCEEDED");
77808
78370
  }
77809
- if (journal.segments.length === 0 && oldestInitialDeploy && Object.keys(stateResources).length === 0) {
78371
+ const survivingOrphans = orphansAfterRollback(baseState, mintedOrphans).orphans ?? [];
78372
+ if (journal.segments.length === 0 && oldestInitialDeploy && Object.keys(stateResources).length === 0 && survivingOrphans.length === 0) {
77810
78373
  await setup.stateBackend.deleteState(stackName, region);
77811
78374
  logger.info(`State for '${stackName}' (${region}) removed (stack fully rolled back).`);
77812
78375
  }
@@ -80793,7 +81356,7 @@ async function importCommand(stackArg, options) {
80793
81356
  }
80794
81357
  }
80795
81358
  const stackState = buildStackState(stackInfo.stackName, targetRegion, rows, templateParser, stateTemplate, existingState, selectiveMode);
80796
- await captureObservedForImportedResources(stackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(stackState, stateTemplate, targetRegion, stateBackend, logger)));
81359
+ await captureObservedForImportedResources(stackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(stackState, stateTemplate, targetRegion, stateBackend, logger)), rebuiltLogicalIdsFrom(rows, stateTemplate));
80797
81360
  const saveOptions = {};
80798
81361
  if (existingEtag) saveOptions.expectedEtag = existingEtag;
80799
81362
  if (migrationPending) saveOptions.migrateLegacy = true;
@@ -81109,6 +81672,30 @@ function collectImportableResources(template) {
81109
81672
  return out;
81110
81673
  }
81111
81674
  /**
81675
+ * The logical ids whose `ResourceState` a run REBUILT from the template — the
81676
+ * discriminator `captureObservedForImportedResources` needs for schema v10's
81677
+ * `observedBaselineRefused` (issue
81678
+ * [#2944](https://github.com/go-to-k/cdkd/issues/2944)).
81679
+ *
81680
+ * It is a function, exported and tested, rather than an inline
81681
+ * `rows.filter(...)` at each call site, because the predicate is the whole
81682
+ * safety property: `buildStackState` overwrites exactly the `'imported'` rows
81683
+ * and PRESERVES every other record in a selective merge, so a predicate that
81684
+ * drifts wider (`rows.map(...)`, or admitting another outcome) silently
81685
+ * re-opens the fifth-writer leak — a preserved record's stale, distrusted
81686
+ * `properties` would be treated as this run's resolution, captured against,
81687
+ * and its refusal cleared. Two inline copies could also drift apart from each
81688
+ * other and from `buildStackState`'s own test.
81689
+ *
81690
+ * Every non-`'imported'` outcome is EXCLUDED, including the ones that sound
81691
+ * harmless: `skipped-out-of-scope` is precisely the selective-merge case, and
81692
+ * `failed` / `skipped-not-found` / `skipped-no-impl` leave whatever the record
81693
+ * already held.
81694
+ */
81695
+ function rebuiltLogicalIdsFrom(rows, template) {
81696
+ return new Set(rows.filter((r) => r.outcome === "imported" && r.physicalId !== void 0 && template.Resources[r.logicalId] !== void 0).map((r) => r.logicalId));
81697
+ }
81698
+ /**
81112
81699
  * Compose a `StackState` from the per-resource import outcomes plus
81113
81700
  * dependency info recovered from the template.
81114
81701
  *
@@ -81148,12 +81735,13 @@ function buildStackState(stackName, region, rows, templateParser, template, exis
81148
81735
  };
81149
81736
  }
81150
81737
  return {
81151
- version: 9,
81738
+ version: 10,
81152
81739
  stackName,
81153
81740
  region,
81154
81741
  resources,
81155
81742
  outputs: existingState?.outputs ?? {},
81156
81743
  ...existingState ? exportNamesCarriedFrom(existingState) : { exportNames: [] },
81744
+ ...existingState ? orphansCarriedFrom(existingState) : {},
81157
81745
  lastModified: Date.now()
81158
81746
  };
81159
81747
  }
@@ -81741,14 +82329,20 @@ const NO_RECORDED_SECRETS = /* @__PURE__ */ new Map();
81741
82329
  * call site's own note (issue
81742
82330
  * [#2828](https://github.com/go-to-k/cdkd/issues/2828)).
81743
82331
  */
81744
- async function captureObservedForImportedResources(stackState, providerRegistry, logger, unsafeObservedBaselineLogicalIds) {
82332
+ async function captureObservedForImportedResources(stackState, providerRegistry, logger, unsafeObservedBaselineLogicalIds, rebuiltLogicalIds) {
81745
82333
  const entries = Object.entries(stackState.resources);
81746
82334
  if (entries.length === 0) return;
81747
82335
  await Promise.all(entries.map(async ([logicalId, resource]) => {
82336
+ if (resource.observedBaselineRefused === true && !rebuiltLogicalIds.has(logicalId)) {
82337
+ logger.debug(`observedProperties capture SKIPPED for preserved ${logicalId} (${resource.resourceType}): a previous 'cdkd import' run refused this record's baseline and this run did not re-import it, so its recorded properties are still the ones that refusal distrusted. Deploy a change to this resource to restore a baseline.`);
82338
+ return;
82339
+ }
81748
82340
  if (unsafeObservedBaselineLogicalIds.has(logicalId)) {
82341
+ resource.observedBaselineRefused = true;
81749
82342
  logger.debug(`observedProperties capture SKIPPED for imported ${logicalId} (${resource.resourceType}): the recorded properties no longer spell the template's dynamic reference, so they cannot position a redaction — capturing an AWS readback against them could persist a resolved secret in plaintext. Drift will compare against the recorded properties for this resource until the next successful deploy.`);
81750
82343
  return;
81751
82344
  }
82345
+ delete resource.observedBaselineRefused;
81752
82346
  try {
81753
82347
  const provider = providerRegistry.getProviderFor({
81754
82348
  resourceType: resource.resourceType,
@@ -81928,7 +82522,7 @@ async function importNestedStackChildrenRecursive(args) {
81928
82522
  childStackState.parentStack = parentStackName;
81929
82523
  childStackState.parentLogicalId = childLogicalId;
81930
82524
  childStackState.parentRegion = parentRegion;
81931
- await captureObservedForImportedResources(childStackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(childStackState, childStateTemplate, childRegion, stateBackend, logger)));
82525
+ await captureObservedForImportedResources(childStackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(childStackState, childStateTemplate, childRegion, stateBackend, logger)), rebuiltLogicalIdsFrom(rows, childTemplate));
81932
82526
  await stateBackend.saveState(childStackName, childRegion, childStackState);
81933
82527
  logger.info(`✓ Nested stack state written: ${childStackName} (${childRegion}) — ${rows.filter((r) => r.outcome === "imported").length} resource(s) imported.`);
81934
82528
  if (childTreeNode.nested.size > 0) await importNestedStackChildrenRecursive({
@@ -91159,4 +91753,4 @@ function buildProgram() {
91159
91753
 
91160
91754
  //#endregion
91161
91755
  export { buildProgram };
91162
- //# sourceMappingURL=program-BEvh5nqs.js.map
91756
+ //# sourceMappingURL=program-CIn_e9xI.js.map