@go-to-k/cdkd 0.283.32 → 0.283.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as getAccountInfo, $t as runDockerForeground, A as cyan, An as getAwsClients, At as S3StateBackend, B as stateKeySecretExposure, Bn as LocalStartServiceError, Bt as escapeRegExp, C as makeCanonicalizePropertiesFn, Cn as canonicalizeRegion, Ct as DiffCalculator, D as renderStatefulReason, Dt as DagBuilder, E as isStatefulRecreateTargetSync, En as processStackMessages, Et as withRetry, F as collectDeclaredOutputNames, Ft as WorkGraph, G as findActionableSilentDrops, Gn as ProvisioningError, Gt as getBootstrapMarkerKey, H as collectInlinePolicyNamesManagedBySiblings, Hn as MissingCdkCliError, Ht as AssetModeResolver, I as collectPublishedOutputNames, It as buildAssetRedirectMap, J as slowCcOperationTimeoutMs, Jn as StackHasActiveImportsError, Jt as validateContainerRepoName, K as findSilentDropProperties, Kn as ResourceTimeoutError, Kt as parseBootstrapMarker, L as exportAliasCollisionScrubWarning, Ln as DeployCancelledError, Lt as createAssetRedirectResolver, M as green, Mn as setAwsClients, Mt as shouldRetainResource, N as red, Nt as AssetPublisher, O as formatResourceLine, On as resolveBucketRegion, Ot as TemplateParser, P as yellow, Pn as CdkdError, Pt as stringifyValue, Q as cfnRefValueFromPhysicalId, Qt as getDockerCmd, R as isExportAliasCollision, Rn as LocalInvokeBuildError$1, Rt as loadPublishableAssetManifest, S as unsupportedFinalSnapshotError, Sn as PARTITION_TABLE, St as applyRoleArnIfSet, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as AssemblyReader, Tt as describeTypeWithThrottleRetry, U as clearOnUpdateRemoval, Un as NestedStackChildDirectDestroyError, Ut as BOOTSTRAP_MARKER_PREFIX, V as IAMRoleProvider, Vt as stripControlChars, W as ProviderRegistry, Wn as PartialFailureError, Wt as ensureAssetStorage, X as isTerminationProtectionPropagationError, Xt as buildDockerImage, Y as disableInstanceApiTermination, Yn as StackTerminationProtectionError, Yt as buildDenyExternalAccessPolicy, Z as IntrinsicFunctionResolver, Zt as formatDockerLoginError, _ as buildFinalSnapshotIdentifier, _n as CFN_TEMPLATE_URL_LIMIT, _t as s3BucketArn, a as DeploymentEventsStore, an as getDefaultStateBucketName, ar as markNonRetryable, at as coerceCfnBoolean, b as isFinalSnapshotError, bn as uploadCfnTemplate, bt as s3BucketRegionalDomainName, c as replayFailedOperations, cn as resolveAutoAssetStorage, ct as readConfigString, d as deleteSkipReason, dn as resolveStateBucketWithDefault, dt as requireConfigObject, en as runDockerStreaming, er as normalizeAwsError, et as refStateLookupFromResource, f as withResourceDeadline, fn as resolveStateBucketWithDefaultAndSource, ft as requireConfigString, g as PRE_DELETE_SNAPSHOT_TYPES, gn as CFN_TEMPLATE_BODY_LIMIT, gt as scrubResourceRecord, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as warnDeprecatedNoPrefixCliFlag, ht as redactSecretsForState, i as DeploymentEventsReader, in as synthesisStatusMessage, ir as isThrottlingError, it as assertRegionMatch, j as gray, jn as resetAwsClients, jt as rebuildClientForBucketRegion, k as bold, kn as AwsClients, kt as LockManager, l as replayRollback, ln as resolveCaptureObservedState, lt as replayWarn, m as computeImplicitDeleteEdges, mn as stateBucketExistenceConfirmed, mt as TEMPLATE_SOURCED_RULES, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as getDockerImageBySourceHash, nr as isMarkedNonRetryable, nt as normalizeAwsTagsToCfn, o as planFailedOps, on as getLegacyStateBucketName, ot as configBooleanRefusal, p as IMPLICIT_DELETE_DEPENDENCIES, pn as resolveUseCdkBootstrapAssets, pt as STATE_SOURCED_CROSS_GENERATION_RULES, q as CloudControlProvider, qn as ResourceUpdateNotSupportedError, qt as validateAssetBucketName, r as DeployEngine, rn as Synthesizer, rr as isRetryableTransientError, rt as resolveExplicitPhysicalId, s as planRollback, sn as resolveApp, st as configStringRefusal, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as AssetManifestLoader, tr as withErrorHandling, tt as WAFv2WebACLProvider, u as UNSPECIFIED_SKIP_REASON, un as resolveSkipPrefix, ut as requireConfigArray, v as ccRoutedFinalSnapshotError, vn as MIGRATE_TMP_PREFIX, vt as s3BucketDomainName, w as extractDeploymentEventError, wn as derivePartitionAndUrlSuffix, wt as INTRINSIC_KEYS, x as refusesFinalSnapshot, xn as expectedOwnerParam, xt as s3BucketWebsiteUrl, y as createPreDeleteFinalSnapshot, yn as findLargeInlineResources, yt as s3BucketDualStackDomainName, z as secretBearingStateKeyWarning, zn as LocalMigrateError, zt as rewriteTemplateAssetReferences } from "./deploy-engine-rYTNzst0.js";
2
+ import { $ as getAccountInfo, $t as formatDockerLoginError, A as cyan, An as resolveBucketRegion, At as TemplateParser, B as stateKeySecretExposure, Bn as LocalInvokeBuildError$1, Bt as loadPublishableAssetManifest, C as makeCanonicalizePropertiesFn, Cn as expectedOwnerParam, Ct as s3BucketWebsiteUrl, D as renderStatefulReason, Dn as AssemblyReader, Dt as describeTypeWithThrottleRetry, E as isStatefulRecreateTargetSync, En as derivePartitionAndUrlSuffix, Et as INTRINSIC_KEYS, F as collectDeclaredOutputNames, Ft as AssetPublisher, G as findActionableSilentDrops, Gn as NestedStackChildDirectDestroyError, Gt as BOOTSTRAP_MARKER_PREFIX, H as collectInlinePolicyNamesManagedBySiblings, Hn as LocalStartServiceError, Ht as escapeRegExp, I as collectPublishedOutputNames, In as CdkdError, It as stringifyValue, J as slowCcOperationTimeoutMs, Jn as ResourceTimeoutError, Jt as parseBootstrapMarker, K as findSilentDropProperties, Kn as PartialFailureError, Kt as ensureAssetStorage, L as exportAliasCollisionScrubWarning, Lt as WorkGraph, M as green, Mn as getAwsClients, Mt as S3StateBackend, N as red, Nn as resetAwsClients, Nt as rebuildClientForBucketRegion, O as formatResourceLine, On as processStackMessages, Ot as withRetry, P as yellow, Pn as setAwsClients, Pt as shouldRetainResource, Q as cfnRefValueFromPhysicalId, Qt as buildDockerImage, R as isExportAliasCollision, Rt as buildAssetRedirectMap, S as unsupportedFinalSnapshotError, Sn as uploadCfnTemplate, St as s3BucketRegionalDomainName, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as canonicalizeRegion, Tt as DiffCalculator, U as clearOnUpdateRemoval, Ut as stripControlChars, V as IAMRoleProvider, Vn as LocalMigrateError, Vt as rewriteTemplateAssetReferences, W as ProviderRegistry, Wn as MissingCdkCliError, Wt as AssetModeResolver, X as isTerminationProtectionPropagationError, Xn as StackHasActiveImportsError, Xt as validateContainerRepoName, Y as disableInstanceApiTermination, Yn as ResourceUpdateNotSupportedError, Yt as validateAssetBucketName, Z as IntrinsicFunctionResolver, Zn as StackTerminationProtectionError, Zt as buildDenyExternalAccessPolicy, _ as buildFinalSnapshotIdentifier, _n as warnDeprecatedNoPrefixCliFlag, _t as redactSecretsForState, a as DeploymentEventsStore, an as Synthesizer, ar as isRetryableTransientError, at as coerceCfnBoolean, b as isFinalSnapshotError, bn as MIGRATE_TMP_PREFIX, bt as s3BucketDomainName, c as replayFailedOperations, cn as getLegacyStateBucketName, ct as readConfigString, d as deleteSkipReason, dn as resolveCaptureObservedState, dt as requireConfigObject, en as getDockerCmd, et as refStateLookupFromResource, f as withResourceDeadline, fn as resolveSkipPrefix, ft as requireConfigString, g as PRE_DELETE_SNAPSHOT_TYPES, gn as stateBucketExistenceConfirmed, gt as maskSecretsInText, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as resolveUseCdkBootstrapAssets, ht as TEMPLATE_SOURCED_RULES, i as DeploymentEventsReader, in as getDockerImageBySourceHash, ir as isMarkedNonRetryable, it as assertRegionMatch, j as gray, jn as AwsClients, jt as LockManager, k as bold, kt as DagBuilder, l as replayRollback, ln as resolveApp, lt as replayWarn, m as computeImplicitDeleteEdges, mn as resolveStateBucketWithDefaultAndSource, mt as STATE_SOURCED_READBACK_RULES, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as runDockerStreaming, nr as normalizeAwsError, nt as normalizeAwsTagsToCfn, o as planFailedOps, on as synthesisStatusMessage, or as isThrottlingError, ot as configBooleanRefusal, p as IMPLICIT_DELETE_DEPENDENCIES, pn as resolveStateBucketWithDefault, pt as STATE_SOURCED_CROSS_GENERATION_RULES, q as CloudControlProvider, qn as ProvisioningError, qt as getBootstrapMarkerKey, r as DeployEngine, rn as AssetManifestLoader, rr as withErrorHandling, rt as resolveExplicitPhysicalId, s as planRollback, sn as getDefaultStateBucketName, sr as markNonRetryable, st as configStringRefusal, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as runDockerForeground, tt as WAFv2WebACLProvider, u as UNSPECIFIED_SKIP_REASON, un as resolveAutoAssetStorage, ut as requireConfigArray, v as ccRoutedFinalSnapshotError, vn as CFN_TEMPLATE_BODY_LIMIT, vt as scrubResourceRecord, w as extractDeploymentEventError, wn as PARTITION_TABLE, wt as applyRoleArnIfSet, x as refusesFinalSnapshot, xn as findLargeInlineResources, xt as s3BucketDualStackDomainName, y as createPreDeleteFinalSnapshot, yn as CFN_TEMPLATE_URL_LIMIT, yt as s3BucketArn, z as secretBearingStateKeyWarning, zn as DeployCancelledError, zt as createAssetRedirectResolver } from "./deploy-engine-Cg54H4pJ.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
4
- import { t as ASGProvider } from "./asg-provider-CVoisQ0L.js";
4
+ import { t as ASGProvider } from "./asg-provider-ID5-zAwf.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
7
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -60644,6 +60644,325 @@ function createIamPrincipalUniqueIdResolver(awsClients) {
60644
60644
  };
60645
60645
  }
60646
60646
  /**
60647
+ * Does any string anywhere in `value` carry a `{{resolve:...}}` dynamic
60648
+ * reference?
60649
+ *
60650
+ * Cheap pre-check so the overwhelming majority of resources — which reference
60651
+ * no secret at all — never build a resolver context and never pay a deep clone
60652
+ * of their property bag.
60653
+ */
60654
+ function containsDynamicReference(value) {
60655
+ if (typeof value === "string") return value.includes("{{resolve:");
60656
+ if (Array.isArray(value)) return value.some(containsDynamicReference);
60657
+ if (value !== null && typeof value === "object") return Object.values(value).some(containsDynamicReference);
60658
+ return false;
60659
+ }
60660
+ /**
60661
+ * Byte-for-byte the threshold `secret-redaction.ts` applies when it builds its
60662
+ * substring needles (`MIN_NEEDLE_LENGTH`). Copied rather than imported because
60663
+ * that constant is module-private there and this file may not widen its
60664
+ * exports; the two must agree or this function would call a leaf secret-bearing
60665
+ * that `redactSecretsForState` will not redact.
60666
+ */
60667
+ const MIN_SECRET_NEEDLE_LENGTH = 4;
60668
+ /**
60669
+ * Does `value` hold a plaintext this pass has recorded as a secret?
60670
+ *
60671
+ * Two branches with deliberately different strictness, and the asymmetry is the
60672
+ * point. A WHOLE-value match is exact evidence at any length, so it is accepted
60673
+ * unconditionally. A SUBSTRING match is only evidence when the plaintext is
60674
+ * long enough to be distinctive: a 1-3 character secret (a JSON key holding
60675
+ * `"0"`) occurs inside unrelated values constantly, and accepting it would mark
60676
+ * half the resource's paths secret-bearing — masking values that are not
60677
+ * secrets and, worse, making `--accept` refuse them.
60678
+ *
60679
+ * So this errs toward NOT marking on the substring branch, and that direction
60680
+ * is chosen rather than inherited: `redactSecretsForState` applies the same
60681
+ * threshold when it builds needles, so a sub-threshold plaintext is not
60682
+ * substituted there either. Marking a path this function's caller cannot
60683
+ * actually redact would produce a `***` with no mechanism behind it.
60684
+ */
60685
+ function carriesRecordedSecret(value, secrets) {
60686
+ if (value === "") return false;
60687
+ if (secrets.has(value)) return true;
60688
+ for (const plaintext of secrets.keys()) if (plaintext.length >= MIN_SECRET_NEEDLE_LENGTH && value.includes(plaintext)) return true;
60689
+ return false;
60690
+ }
60691
+ /**
60692
+ * Is this unresolvable token one whose value is a SECRET by definition?
60693
+ *
60694
+ * `ssm-secure` is the only such spelling, and it is decidable with no lookup:
60695
+ * CloudFormation defines exactly three dynamic-reference services (`ssm`,
60696
+ * `ssm-secure`, `secretsmanager`), cdkd resolves the other two, and
60697
+ * `ssm-secure` is the encrypted one.
60698
+ *
60699
+ * Anything else reaching the survivor arm is not a CloudFormation dynamic
60700
+ * reference at all — it is text that merely looks like one — and treating it as
60701
+ * secret is not the safe direction it appears to be: the path would be masked
60702
+ * to `***`, refused by `--accept`, and pinned to the live value by
60703
+ * {@link preserveLiveValuesAtUnresolvedTokens}, which is permanently stuck
60704
+ * drift with no remedy the user can apply. Such a token is still REPORTED; it
60705
+ * just does not claim to be a secret.
60706
+ */
60707
+ function isSecretBySpelling(token) {
60708
+ return token.startsWith("{{resolve:ssm-secure:");
60709
+ }
60710
+ /**
60711
+ * Every `{{resolve:...}}` token still present in `value`.
60712
+ *
60713
+ * Used to NAME an unresolvable reference without quoting the string it sits in.
60714
+ * That string is a partially substituted leaf: `resolveDynamicReferences`
60715
+ * substitutes token by token, so a leaf holding two references comes back with
60716
+ * the resolvable one already replaced by its PLAINTEXT. Interpolating it into a
60717
+ * log line prints the secret — from the command whose purpose is not to. A
60718
+ * token is a reference NAME and carries no value, so it is always safe to show.
60719
+ */
60720
+ function survivingDynamicReferences(value) {
60721
+ return value.match(/\{\{resolve:[^}]+\}\}/g) ?? [];
60722
+ }
60723
+ /**
60724
+ * Record the dotted path of every leaf holding a `{{resolve:...}}` string.
60725
+ *
60726
+ * The OFFLINE seed for {@link SecretPathSet}: it needs no AWS call, so it is
60727
+ * what remains when resolution fails. Issue #1900's mechanism applied to
60728
+ * positions instead of values — the record's own bags already say WHERE the
60729
+ * references are, and that is the half of the answer a failed lookup does not
60730
+ * take away.
60731
+ *
60732
+ * Necessarily coarser than the resolved answer, because a `{{resolve:ssm:...}}`
60733
+ * naming a plain `String` parameter is PUBLIC config and only the resolver's
60734
+ * TYPE verdict can say so. Marking such a path secret-bearing over-masks it,
60735
+ * which is why the seed is used ONLY on the failure path, where the alternative
60736
+ * is not masking at all.
60737
+ */
60738
+ function collectDynamicReferencePaths(value, into, path = "") {
60739
+ if (typeof value === "string") {
60740
+ if (value.includes("{{resolve:")) into.add(path);
60741
+ return;
60742
+ }
60743
+ if (Array.isArray(value)) {
60744
+ value.forEach((item, i) => collectDynamicReferencePaths(item, into, path === "" ? String(i) : `${path}.${i}`));
60745
+ return;
60746
+ }
60747
+ if (value !== null && typeof value === "object") for (const [k, v] of Object.entries(value)) collectDynamicReferencePaths(v, into, path === "" ? k : `${path}.${k}`);
60748
+ }
60749
+ /**
60750
+ * Re-resolve the SECRET dynamic references (`{{resolve:secretsmanager:...}}`,
60751
+ * and an `{{resolve:ssm:...}}` naming a `SecureString`) held by a bag read back
60752
+ * out of cdkd STATE (issue #1914).
60753
+ *
60754
+ * State stores the unresolved EXPRESSION — that is the GHSA-p5qg-v9gv-hc7w
60755
+ * redaction — while a provider's `readCurrentState` snapshot necessarily holds
60756
+ * the resolved PLAINTEXT, because that is what AWS was given at deploy time.
60757
+ * Nothing in `cdkd drift` reconciled the two, and all three of the command's
60758
+ * modes broke on it: the comparison reported permanent phantom drift and
60759
+ * printed the plaintext as the AWS-current side, `--accept` persisted that
60760
+ * plaintext back into `state.json`, and `--revert` shipped the literal
60761
+ * `{{resolve:...}}` string to the live resource.
60762
+ *
60763
+ * The counterpart of the rollback replay's `resolveReplayProps`
60764
+ * (`src/deployment/rollback-executor.ts`), and for the same reason: both
60765
+ * commands are synth-free, so the expression string in the persisted record is
60766
+ * the only thing there is to resolve from. Every plaintext is recorded into
60767
+ * `secrets` so the caller can redact it back out of anything it PRINTS or
60768
+ * PERSISTS, and every path that produced one into `secretPaths` so the caller
60769
+ * can act on a value it does NOT recognise there — see {@link redactDriftChanges}.
60770
+ *
60771
+ * A `{{resolve:...}}` token that SURVIVES the pass is reported through
60772
+ * `onUnresolved` and left in place — it is not an error. The resolver's
60773
+ * unsupported-service arm (`ssm-secure:` is the live example) warns and returns
60774
+ * the literal, and `cdkd deploy` resolves through the very same code, so AWS is
60775
+ * ALREADY holding that literal string and state records it. Replaying it is
60776
+ * therefore a correct no-op, and failing the resource over it would take every
60777
+ * other drifted property on that resource down with it (plus exit 2). The
60778
+ * report exists so the user learns cdkd is shipping a token it cannot resolve,
60779
+ * which is a real thing to know and not a reason to stop.
60780
+ *
60781
+ * Only the TOKENS are reported, never the leaf that held them: the leaf is
60782
+ * partially substituted by this point, so a mixed `secretsmanager` +
60783
+ * `ssm-secure` string comes back carrying real plaintext.
60784
+ *
60785
+ * Returns the input by identity when it holds no dynamic reference, so the
60786
+ * non-secret path is unchanged down to object identity.
60787
+ */
60788
+ async function resolveStateSecretExpressions(props, resolver, secrets, options = {}) {
60789
+ if (!containsDynamicReference(props)) return props;
60790
+ const { secretPaths, onUnresolved } = options;
60791
+ const ctx = {
60792
+ template: { Resources: {} },
60793
+ resources: {},
60794
+ recordedSecretValues: secrets
60795
+ };
60796
+ const walk = async (v, path) => {
60797
+ if (typeof v === "string") {
60798
+ if (!v.includes("{{resolve:")) return v;
60799
+ const resolved = await resolver.resolveDynamicReferences(v, ctx);
60800
+ const survivors = survivingDynamicReferences(resolved).filter((t) => v.includes(t));
60801
+ if (survivors.length > 0) {
60802
+ if (survivors.some(isSecretBySpelling)) secretPaths?.add(path);
60803
+ onUnresolved?.(survivors, path);
60804
+ }
60805
+ if (carriesRecordedSecret(resolved, secrets)) secretPaths?.add(path);
60806
+ return resolved;
60807
+ }
60808
+ if (Array.isArray(v)) {
60809
+ const out = new Array(v.length);
60810
+ for (let i = 0; i < v.length; i++) out[i] = await walk(v[i], path === "" ? String(i) : `${path}.${i}`);
60811
+ return out;
60812
+ }
60813
+ if (v !== null && typeof v === "object") {
60814
+ const out = {};
60815
+ for (const [k, val] of Object.entries(v)) out[k] = await walk(val, path === "" ? k : `${path}.${k}`);
60816
+ return out;
60817
+ }
60818
+ return v;
60819
+ };
60820
+ return await walk(props, "");
60821
+ }
60822
+ /**
60823
+ * Is a drift reported at `path` positioned on, or above, a known secret?
60824
+ *
60825
+ * The PREFIX direction is the one that matters: a secret sits at a leaf (or at
60826
+ * an array the comparator never descends), while a drift can be reported at any
60827
+ * ancestor of it — `Environment` rather than
60828
+ * `Environment.Variables.SECRET_PASSWORD` — whenever the two sides disagree
60829
+ * about the shape rather than the value. The reverse containment is checked too
60830
+ * and costs nothing.
60831
+ */
60832
+ function isSecretBearingPath(path, secretPaths) {
60833
+ if (secretPaths.size === 0) return false;
60834
+ for (const secretPath of secretPaths) if (secretPath === path || secretPath.startsWith(`${path}.`) || path.startsWith(`${secretPath}.`)) return true;
60835
+ return false;
60836
+ }
60837
+ /**
60838
+ * Redact one side of a drift entry.
60839
+ *
60840
+ * Two passes, and the second is the one issue #1914's review turned up. The
60841
+ * VALUE pass rewrites any recorded plaintext back onto its own
60842
+ * `{{resolve:...}}` expression, which is exact and is all that is needed while
60843
+ * the secret AWS holds is the secret the reference resolves to today.
60844
+ *
60845
+ * It stops being enough the moment those two differ, and they differ for the
60846
+ * most ordinary reason there is: a Secrets Manager ROTATION. The deployed
60847
+ * resource then still holds the PREVIOUS secret while the re-resolved baseline
60848
+ * holds the new one, so the path drifts and the AWS side matches no key in a
60849
+ * map built from today's value — it is a real secret that would be printed
60850
+ * verbatim and persisted by `--accept`. An out-of-band edit is
60851
+ * indistinguishable from it at this layer, so the POSITION decides: at a path
60852
+ * known to carry a secret, the only value ever shown is the expression itself,
60853
+ * and anything else becomes {@link SECRET_MASK}.
60854
+ *
60855
+ * `undefined` and `null` are exempt — an absent or null key discloses nothing,
60856
+ * and masking them would turn "AWS does not have this key" into "AWS has
60857
+ * something secret here", which is both wrong and less useful.
60858
+ */
60859
+ function redactDriftValue(value, secrets, secretBearing) {
60860
+ const redacted = redactSecretsForState(value, secrets);
60861
+ if (!secretBearing) return redacted;
60862
+ if (redacted === void 0 || redacted === null) return redacted;
60863
+ if (typeof redacted === "string" && isWholeDynamicReference(redacted)) return redacted;
60864
+ return "***";
60865
+ }
60866
+ /**
60867
+ * Is the WHOLE string a single `{{resolve:...}}` token and nothing else?
60868
+ *
60869
+ * Byte-for-byte `secret-redaction.ts`'s `isSingleDynamicReferenceToken`,
60870
+ * including the `[^{}]*` character class. The difference from `[^}]+` is
60871
+ * narrow and worth stating correctly: it is a `{` INSIDE the token, e.g.
60872
+ * `{{resolve:ssm:/a{b}}`. (A previous version of this comment claimed
60873
+ * `{{resolve:a}}{{resolve:b}}` would pass under `[^}]+` — it would not, since
60874
+ * `[^}]+` cannot span the `}` in the middle either.) Matching the sibling
60875
+ * exactly is what matters here: this predicate decides whether a value may be
60876
+ * shown INSTEAD of `SECRET_MASK`, and `redactByPath` uses its copy to decide
60877
+ * whether a leaf is already an expression, so a disagreement would let one of
60878
+ * them treat as a token what the other treats as data. Copied rather than
60879
+ * imported because that helper is module-private and this file may not widen
60880
+ * that module's exports.
60881
+ */
60882
+ function isWholeDynamicReference(value) {
60883
+ return /^\{\{resolve:[^{}]*\}\}$/.test(value);
60884
+ }
60885
+ /**
60886
+ * Why `--accept` must not persist this drift, or `undefined` when it may.
60887
+ *
60888
+ * Shared by the WRITE (`runAccept`) and the PLAN (`printAcceptPlan`) so the two
60889
+ * cannot disagree — a `--dry-run` that promises a write the real run refuses is
60890
+ * worse than either behaviour on its own.
60891
+ *
60892
+ * Two refusals, both about a value cdkd could not identify rather than about
60893
+ * secrecy as such:
60894
+ *
60895
+ * - A masked VALUE says AWS holds something at a known-secret path that is not
60896
+ * what the reference resolves to. Writing `***` would corrupt the baseline
60897
+ * and make the next deploy push the literal mask at AWS.
60898
+ * - A masked PATH says the readback answered with a map KEYED by a secret.
60899
+ * `setAtPath` would then create a key literally named `***` — and when
60900
+ * `awsValue` is `undefined` it would INSERT that key rather than removing the
60901
+ * real one, so the value check alone does not cover it.
60902
+ *
60903
+ * The path check is what makes the value exemptions safe: `redactDriftValue`
60904
+ * lets `undefined` / `null` / a whole expression through unmasked, which is
60905
+ * right for a value and says nothing about the path it sits at.
60906
+ */
60907
+ function acceptRefusalReason(change, maskedPaths) {
60908
+ if (!maskedPaths.has(change.path)) return void 0;
60909
+ if (change.path.includes("***")) return "its property NAME came back carrying a secret value, so cdkd cannot name the key to write — and no redaction pass can clear it either, since they all walk values and never keys. ROTATE the secret; that is the only remedy";
60910
+ if (change.awsValue === void 0) return "AWS no longer reports it, and accepting an absence DELETES the key — which at or above a secret dynamic reference would erase the reference out of cdkd state. Use --revert to push it back, or re-deploy";
60911
+ return "it carries a secret dynamic reference and the value AWS currently holds is not the one the reference resolves to";
60912
+ }
60913
+ /**
60914
+ * Redact resolved secret plaintext out of the drift entries that get REPORTED
60915
+ * (issue #1914).
60916
+ *
60917
+ * Both sides need it, for different reasons. `stateValue` comes from the
60918
+ * re-resolved comparison baseline, so at a secret leaf it now holds the
60919
+ * plaintext the state record deliberately does not store. `awsValue` is the
60920
+ * live readback, which holds that plaintext by construction. Either one
60921
+ * reaching `writeHumanReport` / `writeJsonReport` / the `--accept` and
60922
+ * `--revert` plans is the disclosure the redaction exists to prevent.
60923
+ *
60924
+ * Redacting `awsValue` also settles what `--accept` PERSISTS: that is the value
60925
+ * it writes into the baseline, so a secret one arrives at the state write
60926
+ * already carrying its expression — or, when it could not be recognised, as
60927
+ * {@link SECRET_MASK}, which `runAccept` refuses to persist.
60928
+ *
60929
+ * The PATH is redacted too. `redactSecretsForState` walks values and never
60930
+ * object KEYS, so a readback that answers with a map keyed by a secret (a
60931
+ * Lambda env var NAMED with one) renders as
60932
+ * `Environment.Variables.<plaintext>` in every printer. It is masked here
60933
+ * rather than assumed impossible, and a masked path also marks the change
60934
+ * secret-bearing, so `--accept` will not write a key it can no longer name.
60935
+ *
60936
+ * Returns the input array by identity when there is nothing to act on.
60937
+ */
60938
+ function redactDriftChanges(changes, secrets, secretPaths) {
60939
+ if (secrets.size === 0 && secretPaths.size === 0) return {
60940
+ changes,
60941
+ maskedPaths: /* @__PURE__ */ new Set()
60942
+ };
60943
+ const maskedPaths = /* @__PURE__ */ new Set();
60944
+ const redacted = [];
60945
+ for (const change of changes) {
60946
+ const maskedPath = maskSecretsInText(change.path, secrets);
60947
+ const positionIsSecret = isSecretBearingPath(change.path, secretPaths);
60948
+ const nameCarriesSecret = maskedPath !== change.path;
60949
+ const secretBearing = positionIsSecret || nameCarriesSecret;
60950
+ if (secretPaths.has(change.path) && change.awsValue === void 0) continue;
60951
+ const stateValue = redactDriftValue(change.stateValue, secrets, secretBearing);
60952
+ const awsValue = redactDriftValue(change.awsValue, secrets, secretBearing);
60953
+ if (maskedPath !== change.path || secretBearing && awsValue === "***" || secretBearing && change.awsValue === void 0) maskedPaths.add(maskedPath);
60954
+ redacted.push({
60955
+ path: maskedPath,
60956
+ stateValue,
60957
+ awsValue
60958
+ });
60959
+ }
60960
+ return {
60961
+ changes: redacted,
60962
+ maskedPaths
60963
+ };
60964
+ }
60965
+ /**
60647
60966
  * Run drift detection for one stack and shape the per-resource outcomes
60648
60967
  * into a {@link StackDriftReport}. The state object + etag are stored on
60649
60968
  * the report so `--accept` can write back without a re-read, and
@@ -60656,6 +60975,8 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
60656
60975
  return await withStackName(stackName, async () => {
60657
60976
  const outcomes = [];
60658
60977
  const state = result.state;
60978
+ const logger = getLogger();
60979
+ const secretResolver = new IntrinsicFunctionResolver(region);
60659
60980
  const entries = Object.entries(state.resources ?? {}).sort(([a], [b]) => a.localeCompare(b));
60660
60981
  for (const [logicalId, resource] of entries) {
60661
60982
  if (providerRegistry.shouldSkipResource(resource.resourceType)) continue;
@@ -60715,19 +61036,46 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
60715
61036
  const unorderedPaths = provider.getDriftUnorderedPaths ? provider.getDriftUnorderedPaths(resource.resourceType) : [];
60716
61037
  const useObserved = resource.observedProperties !== void 0;
60717
61038
  const baseline = useObserved ? resource.observedProperties : resource.properties ?? {};
61039
+ const secrets = /* @__PURE__ */ new Map();
61040
+ const secretPaths = /* @__PURE__ */ new Set();
61041
+ const seededSecretPaths = /* @__PURE__ */ new Set();
61042
+ collectDynamicReferencePaths(baseline, seededSecretPaths);
61043
+ if (useObserved) collectDynamicReferencePaths(resource.properties ?? {}, seededSecretPaths);
61044
+ const unresolvedTokens = /* @__PURE__ */ new Set();
61045
+ const noteUnresolved = (tokens) => {
61046
+ for (const token of tokens) unresolvedTokens.add(token);
61047
+ };
61048
+ let comparisonBaseline = baseline;
61049
+ let secretResolutionFailed = false;
61050
+ try {
61051
+ comparisonBaseline = await resolveStateSecretExpressions(baseline, secretResolver, secrets, {
61052
+ secretPaths,
61053
+ onUnresolved: noteUnresolved
61054
+ });
61055
+ if (useObserved) await resolveStateSecretExpressions(resource.properties ?? {}, secretResolver, secrets, {
61056
+ secretPaths,
61057
+ onUnresolved: noteUnresolved
61058
+ });
61059
+ } catch (err) {
61060
+ secretResolutionFailed = true;
61061
+ comparisonBaseline = baseline;
61062
+ logger.warn(`${logicalId} (${resource.resourceType}): could not resolve the dynamic reference(s) this resource's state records, so its secret-bearing properties are NOT compared — ${maskSecretsInText(err instanceof Error ? err.message : String(err), secrets)}`);
61063
+ secrets.clear();
61064
+ }
61065
+ if (unresolvedTokens.size > 0) logger.warn(`${logicalId} (${resource.resourceType}): cdkd cannot resolve ${maskSecretsInText([...unresolvedTokens].join(", "), secrets)} — those properties are NOT compared. A revert leaves a property whose WHOLE value is one of these tokens untouched; where a token is EMBEDDED in a longer string, the revert writes that string with the token literal, exactly as 'cdkd deploy' does — so a resolved value AWS holds there WOULD be overwritten. cdkd resolves 'secretsmanager' and 'ssm' references only.`);
60718
61066
  const observedIgnorePaths = useObserved ? undeclaredEmptyObservedKeys(resource.observedProperties, resource.properties ?? {}) : [];
60719
- const normalized = await canonicalizePrincipalUniqueIds(baseline, aws, resolvePrincipalUniqueId);
61067
+ const normalized = await canonicalizePrincipalUniqueIds(comparisonBaseline, aws, resolvePrincipalUniqueId);
60720
61068
  const protocolNormalized = canonicalizeIpProtocols(normalized.baseline, normalized.aws, resource.resourceType);
60721
61069
  const canonicalized = provider.canonicalizeDriftProperties ? {
60722
61070
  baseline: provider.canonicalizeDriftProperties(resource.resourceType, protocolNormalized.baseline),
60723
61071
  aws: provider.canonicalizeDriftProperties(resource.resourceType, protocolNormalized.aws)
60724
61072
  } : protocolNormalized;
60725
- const changes = calculateResourceDrift(canonicalized.baseline, canonicalized.aws, {
61073
+ const reported = redactDriftChanges(calculateResourceDrift(canonicalized.baseline, canonicalized.aws, {
60726
61074
  ignorePaths: observedIgnorePaths.length ? [...ignorePaths, ...observedIgnorePaths] : ignorePaths,
60727
61075
  unionWalkObjects: useObserved,
60728
61076
  unorderedPaths
60729
- });
60730
- if (changes.length === 0) outcomes.push({
61077
+ }), secrets, secretResolutionFailed ? seededSecretPaths : secretPaths);
61078
+ if (reported.changes.length === 0) outcomes.push({
60731
61079
  kind: "clean",
60732
61080
  logicalId,
60733
61081
  resourceType: resource.resourceType
@@ -60736,8 +61084,10 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
60736
61084
  kind: "drifted",
60737
61085
  logicalId,
60738
61086
  resourceType: resource.resourceType,
60739
- changes,
60740
- awsProperties: aws
61087
+ ...reported,
61088
+ awsProperties: aws,
61089
+ secrets,
61090
+ referencesUnresolved: secretResolutionFailed || unresolvedTokens.size > 0
60741
61091
  });
60742
61092
  }
60743
61093
  return {
@@ -60779,6 +61129,20 @@ function buildReadCurrentStateContext(state, excludedLogicalId) {
60779
61129
  }
60780
61130
  return { siblings };
60781
61131
  }
61132
+ /**
61133
+ * Read the value at a dotted path, or `undefined` when any segment is missing.
61134
+ * The read counterpart of {@link setAtPath}, and it parses the same subset:
61135
+ * the drift comparator only synthesizes paths through plain objects.
61136
+ */
61137
+ function getAtPath(source, path) {
61138
+ if (path.length === 0) return source;
61139
+ let cursor = source;
61140
+ for (const segment of path.split(".")) {
61141
+ if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) return void 0;
61142
+ cursor = cursor[segment];
61143
+ }
61144
+ return cursor;
61145
+ }
60782
61146
  function setAtPath(target, path, value) {
60783
61147
  if (path.length === 0) return;
60784
61148
  const segments = path.split(".");
@@ -60831,13 +61195,27 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
60831
61195
  const hasObserved = existing.observedProperties !== void 0;
60832
61196
  const baselineSource = hasObserved ? existing.observedProperties : existing.properties ?? {};
60833
61197
  const newBaseline = JSON.parse(JSON.stringify(baselineSource));
60834
- for (const change of outcome.changes) setAtPath(newBaseline, change.path, change.awsValue);
61198
+ const accepted = [];
61199
+ for (const change of outcome.changes) {
61200
+ const refusal = acceptRefusalReason(change, outcome.maskedPaths);
61201
+ if (refusal !== void 0) {
61202
+ logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): not accepting '${change.path}' — ${refusal}, so cdkd will not write it to state. Run 'cdkd drift ${report.stackName} --revert' to push the referenced value back to AWS, or re-deploy if the reference changed.`);
61203
+ continue;
61204
+ }
61205
+ setAtPath(newBaseline, change.path, change.awsValue);
61206
+ accepted.push(change);
61207
+ }
61208
+ const redactedBaseline = redactSecretsForState(newBaseline, outcome.secrets, existing.properties ?? {}, STATE_SOURCED_READBACK_RULES);
61209
+ for (const change of accepted) {
61210
+ if (deepEqualUnordered(getAtPath(redactedBaseline, change.path), change.awsValue)) continue;
61211
+ logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): '${change.path}' was NOT recorded — cdkd state holds an unresolved '{{resolve:...}}' reference at that property, and the reference wins over an accepted value. Change the template if that reference is wrong.`);
61212
+ }
60835
61213
  resources[outcome.logicalId] = hasObserved ? {
60836
61214
  ...existing,
60837
- observedProperties: newBaseline
61215
+ observedProperties: redactedBaseline
60838
61216
  } : {
60839
61217
  ...existing,
60840
- properties: newBaseline
61218
+ properties: redactedBaseline
60841
61219
  };
60842
61220
  }
60843
61221
  const newState = {
@@ -61198,6 +61576,57 @@ function mergeUntemplatedValue(awsValue, desiredValue) {
61198
61576
  return merged;
61199
61577
  }
61200
61578
  /**
61579
+ * Replace every leaf of a revert payload that still holds a `{{resolve:...}}`
61580
+ * token with the value AWS currently has at that position (issue #1914,
61581
+ * round-3 review).
61582
+ *
61583
+ * The round-3 change downgraded an unresolvable reference from a per-resource
61584
+ * failure to a warning, on the premise that `cdkd deploy` resolves through the
61585
+ * resolver's same unsupported-service arm — so AWS already holds the literal
61586
+ * and replaying it is a no-op. **That premise holds only for records cdkd
61587
+ * deployed.** CloudFormation resolves `ssm-secure` SERVER-side, so a record
61588
+ * adopted by `cdkd import --migrate-from-cloudformation` has the literal token
61589
+ * in state while the live resource holds the PLAINTEXT. `diffAt` skips such a
61590
+ * leaf, so it never drifts — but `buildRevertNewProperties` overlays the whole
61591
+ * top-level subtree when any SIBLING key drifts, which would push the literal
61592
+ * token over a resolved value. That is defect 1 of this issue surviving in a
61593
+ * narrower case, on a live AWS write.
61594
+ *
61595
+ * Deciding by PROVENANCE would need a flag state does not carry. Deciding by
61596
+ * what AWS actually holds needs nothing and is exact in both directions: for a
61597
+ * cdkd-deployed record AWS holds the token, so copying it back is the same
61598
+ * no-op the premise described; for a migrated record AWS holds the resolved
61599
+ * value, which is left untouched.
61600
+ *
61601
+ * Falls back to KEEPING the token wherever AWS has nothing at that position —
61602
+ * that is what `cdkd deploy` sends, so it is the safe residual rather than
61603
+ * dropping a property the resource may require. Array descent is positional
61604
+ * and bails to that same residual on any length mismatch, since a reordered
61605
+ * readback cannot be positioned against.
61606
+ */
61607
+ function preserveLiveValuesAtUnresolvedTokens(send, awsProperties, secrets) {
61608
+ const walk = (value, live) => {
61609
+ if (typeof value === "string" && value.includes("{{resolve:")) {
61610
+ if (!isWholeDynamicReference(value)) return value;
61611
+ if (live === void 0) return value;
61612
+ if (typeof live === "string" && live.length >= MIN_SECRET_NEEDLE_LENGTH && isSecretBySpelling(value)) secrets.set(live, value);
61613
+ return live;
61614
+ }
61615
+ if (Array.isArray(value)) {
61616
+ const liveItems = Array.isArray(live) && live.length === value.length ? live : void 0;
61617
+ return value.map((item, i) => walk(item, liveItems?.[i]));
61618
+ }
61619
+ if (value !== null && typeof value === "object") {
61620
+ const liveObject = isPlainRecord(live) ? live : void 0;
61621
+ const out = {};
61622
+ for (const [k, v] of Object.entries(value)) out[k] = walk(v, liveObject?.[k]);
61623
+ return out;
61624
+ }
61625
+ return value;
61626
+ };
61627
+ return walk(send, awsProperties);
61628
+ }
61629
+ /**
61201
61630
  * Build the `newProperties` object passed to `provider.update` during
61202
61631
  * `--revert`. Strategy:
61203
61632
  *
@@ -61371,11 +61800,13 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
61371
61800
  let totalFailed = 0;
61372
61801
  let totalUnsupported = 0;
61373
61802
  let totalSucceeded = 0;
61803
+ let totalUnresolvable = 0;
61374
61804
  for (const report of reports) {
61375
61805
  const driftedOutcomes = report.outcomes.filter((o) => o.kind === "drifted");
61376
61806
  if (driftedOutcomes.length === 0) continue;
61377
61807
  await lockManager.acquireLock(report.stackName, report.region, owner, "drift-revert");
61378
61808
  const narrowedByLogicalId = /* @__PURE__ */ new Map();
61809
+ const revertSecretResolver = new IntrinsicFunctionResolver(report.region);
61379
61810
  try {
61380
61811
  await runWithConcurrency(driftedOutcomes.map((outcome) => async () => {
61381
61812
  const stateResource = report.state.resources[outcome.logicalId];
@@ -61388,28 +61819,53 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
61388
61819
  resourceType: outcome.resourceType,
61389
61820
  provisionedBy: stateResource.provisionedBy
61390
61821
  }).provider;
61391
- const desiredProperties = stateResource.observedProperties ?? stateResource.properties ?? {};
61392
- const newProperties = buildRevertNewProperties(outcome.changes, desiredProperties, outcome.awsProperties, { preserveUntemplated: stateResource.observedProperties === void 0 });
61822
+ const secrets = /* @__PURE__ */ new Map();
61823
+ const revertBaseline = stateResource.observedProperties ?? stateResource.properties ?? {};
61824
+ const unresolvedTokens = /* @__PURE__ */ new Set();
61825
+ const noteUnresolved = (tokens) => {
61826
+ for (const token of tokens) unresolvedTokens.add(token);
61827
+ };
61828
+ let desiredProperties;
61393
61829
  try {
61394
- const updateResult = await withRetry(() => provider.update(outcome.logicalId, stateResource.physicalId, outcome.resourceType, newProperties, outcome.awsProperties, { desiredFromAwsReadback: true }), outcome.logicalId, { logger: { debug: (msg) => logger.debug(msg) } });
61830
+ desiredProperties = await resolveStateSecretExpressions(revertBaseline, revertSecretResolver, secrets, { onUnresolved: noteUnresolved });
61831
+ if (stateResource.observedProperties !== void 0) await resolveStateSecretExpressions(stateResource.properties ?? {}, revertSecretResolver, secrets, { onUnresolved: noteUnresolved });
61832
+ } catch (err) {
61833
+ totalUnresolvable++;
61834
+ logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not re-resolve the dynamic reference(s) this resource's state records — ${maskSecretsInText(err instanceof Error ? err.message : String(err), secrets)}`);
61835
+ return;
61836
+ }
61837
+ const unrevertablePaths = outcome.changes.map((c) => c.path).filter((path) => (path.split(".", 1)[0] ?? "").includes("***"));
61838
+ if (unrevertablePaths.length > 0) logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): cannot revert ${unrevertablePaths.join(", ")} — cdkd cannot name the property, so it is left as AWS has it. ROTATE the secret that leaked into the property name.`);
61839
+ if (unresolvedTokens.size > 0) logger.warn(` ! [revert] ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): cdkd cannot resolve ${maskSecretsInText([...unresolvedTokens].join(", "), secrets)} — a property whose WHOLE value is one of these tokens is left UNCHANGED by this revert. Where a token is EMBEDDED in a longer string, that string is written with the token literal, exactly as 'cdkd deploy' does, so a resolved value AWS holds there is overwritten.`);
61840
+ let newProperties;
61841
+ try {
61842
+ const overlaid = buildRevertNewProperties(outcome.changes, desiredProperties, outcome.awsProperties, { preserveUntemplated: stateResource.observedProperties === void 0 });
61843
+ newProperties = unresolvedTokens.size > 0 ? preserveLiveValuesAtUnresolvedTokens(overlaid, outcome.awsProperties, secrets) : overlaid;
61844
+ } catch (err) {
61845
+ totalFailed++;
61846
+ logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not build the revert payload — ${maskSecretsInText(err instanceof Error ? err.message : String(err), secrets)}`);
61847
+ return;
61848
+ }
61849
+ try {
61850
+ const updateResult = await withRetry(() => provider.update(outcome.logicalId, stateResource.physicalId, outcome.resourceType, newProperties, outcome.awsProperties, { desiredFromAwsReadback: true }), outcome.logicalId, { logger: { debug: (msg) => logger.debug(maskSecretsInText(msg, secrets)) } });
61395
61851
  totalSucceeded++;
61396
61852
  logger.info(` ✓ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): reverted.`);
61397
61853
  try {
61398
61854
  if (updateResult?.effectiveProperties) {
61399
61855
  const delta = collectNarrowedTopLevelKeys(newProperties, updateResult.effectiveProperties);
61400
- if (Object.keys(delta).length > 0) narrowedByLogicalId.set(outcome.logicalId, delta);
61856
+ if (Object.keys(delta).length > 0) narrowedByLogicalId.set(outcome.logicalId, redactSecretsForState(delta, secrets, revertBaseline, STATE_SOURCED_READBACK_RULES));
61401
61857
  }
61402
61858
  } catch (captureErr) {
61403
- logger.warn(` ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): reverted, but the provider's reported effective properties could not be read — ${captureErr instanceof Error ? captureErr.message : String(captureErr)}`);
61859
+ logger.warn(` ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): reverted, but the provider's reported effective properties could not be read — ${maskSecretsInText(captureErr instanceof Error ? captureErr.message : String(captureErr), secrets)}`);
61404
61860
  }
61405
61861
  } catch (err) {
61406
61862
  if (err instanceof ResourceUpdateNotSupportedError) {
61407
61863
  totalUnsupported++;
61408
- logger.warn(` ⊘ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not revert — ${err.message}`);
61864
+ logger.warn(` ⊘ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not revert — ${maskSecretsInText(err.message, secrets)}`);
61409
61865
  return;
61410
61866
  }
61411
61867
  totalFailed++;
61412
- const msg = err instanceof Error ? err.message : String(err);
61868
+ const msg = maskSecretsInText(err instanceof Error ? err.message : String(err), secrets);
61413
61869
  logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): AWS update failed — ${msg}`);
61414
61870
  }
61415
61871
  }), concurrency);
@@ -61467,10 +61923,11 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
61467
61923
  }
61468
61924
  const summaryParts = [`${totalSucceeded} reverted`];
61469
61925
  if (totalUnsupported > 0) summaryParts.push(`${totalUnsupported} update-not-supported`);
61926
+ if (totalUnresolvable > 0) summaryParts.push(`${totalUnresolvable} reference-unresolvable`);
61470
61927
  if (totalFailed > 0) summaryParts.push(`${totalFailed} failed`);
61471
61928
  logger.info(`\nRevert summary: ${summaryParts.join(", ")}.`);
61472
61929
  if (totalUnsupported > 0) logger.warn(`${totalUnsupported} resource(s) cannot be reverted in place — re-deploy the stack with cdkd deploy --replace, or destroy + redeploy to push the cdkd-state values back into AWS.`);
61473
- if (totalFailed > 0 || totalUnsupported > 0) throw new PartialFailureError(`Revert completed with ${totalFailed + totalUnsupported} resource error(s) (${totalFailed} AWS update failure(s), ${totalUnsupported} update-not-supported). Re-run 'cdkd drift <stack>' to see the remaining drift, then 'cdkd drift <stack> --revert' to retry.`);
61930
+ if (totalFailed > 0 || totalUnsupported > 0 || totalUnresolvable > 0) throw new PartialFailureError(`Revert completed with ${totalFailed + totalUnsupported + totalUnresolvable} resource error(s) (${totalFailed} AWS update failure(s), ${totalUnsupported} update-not-supported, ${totalUnresolvable} whose dynamic reference(s) could not be resolved — those never reached provider.update; grant the caller secretsmanager:GetSecretValue / ssm:GetParameter, or fix the reference). Re-run 'cdkd drift <stack>' to see the remaining drift, then 'cdkd drift <stack> --revert' to retry.`);
61474
61931
  }
61475
61932
  /**
61476
61933
  * Print the planned state mutations for `--accept` (no AWS calls). One
@@ -61485,7 +61942,14 @@ function printAcceptPlan(reports) {
61485
61942
  process.stdout.write(`\nPlan (--accept): update cdkd state for ${report.stackName} (${report.region}):\n`);
61486
61943
  for (const o of drifted) {
61487
61944
  process.stdout.write(` ~ ${o.logicalId} (${o.resourceType})\n`);
61488
- for (const change of o.changes) process.stdout.write(` ${change.path}: ${formatScalar(change.stateValue)} -> ${formatScalar(change.awsValue)}\n`);
61945
+ for (const change of o.changes) {
61946
+ const refusal = acceptRefusalReason(change, o.maskedPaths);
61947
+ if (refusal !== void 0) {
61948
+ process.stdout.write(` ${change.path}: SKIPPED — ${refusal}\n`);
61949
+ continue;
61950
+ }
61951
+ process.stdout.write(` ${change.path}: ${formatScalar(change.stateValue)} -> ${formatScalar(change.awsValue)}\n`);
61952
+ }
61489
61953
  }
61490
61954
  }
61491
61955
  }
@@ -61506,20 +61970,23 @@ function printRevertPlan(reports) {
61506
61970
  const stateResource = report.state.resources[o.logicalId];
61507
61971
  if (stateResource) {
61508
61972
  const preserved = findRevertPreservedTagKeys(o.changes, stateResource.observedProperties ?? stateResource.properties ?? {}, o.awsProperties);
61509
- if (preserved.length > 0) {
61973
+ const cannotMaskKeys = o.referencesUnresolved;
61974
+ if (cannotMaskKeys && preserved.length > 0) process.stdout.write(` ! ${preserved.length} AWS-authored tag(s) will be preserved, but cdkd could not resolve this resource's dynamic reference(s), so their names are withheld — they come from the live readback and cannot be checked for secrets without them.\n`);
61975
+ if (!cannotMaskKeys && preserved.length > 0) {
61510
61976
  const tagWord = preserved.length === 1 ? "tag" : "tags";
61511
61977
  process.stdout.write(` ! reverting this tag list KEEPS ${preserved.length} AWS-authored ${tagWord} the baseline does not carry:\n`);
61512
- for (const path of preserved) process.stdout.write(` ${path}\n`);
61978
+ for (const path of preserved) process.stdout.write(` ${maskSecretsInText(path, o.secrets)}\n`);
61513
61979
  const othersRevert = stateResource.observedProperties === void 0 ? "" : `Every other tag reverts normally. `;
61514
61980
  process.stdout.write(` ${othersRevert}A service may require these (ECS needs AmazonECSManaged for managed scaling); 'aws:'-prefixed keys are AWS-reserved and cannot be removed by hand.\n`);
61515
61981
  }
61516
61982
  }
61517
61983
  if (stateResource && stateResource.observedProperties === void 0) {
61518
61984
  const unbaselined = findRevertUnbaselinedAwsKeys(o.changes, stateResource.properties ?? {}, o.awsProperties);
61519
- if (unbaselined.length > 0) {
61985
+ if (o.referencesUnresolved && unbaselined.length > 0) process.stdout.write(` ! ${unbaselined.length} AWS-authored value(s) will be left untouched, but cdkd could not resolve this resource's dynamic reference(s), so their paths are withheld — they come from the live readback and cannot be checked for secrets without them.\n`);
61986
+ else if (unbaselined.length > 0) {
61520
61987
  const word = unbaselined.length === 1 ? "value" : "values";
61521
61988
  process.stdout.write(` ! this resource has no observed-capture baseline, so the revert pushes the raw TEMPLATE and LEAVES ${unbaselined.length} AWS-authored ${word} untouched:\n`);
61522
- for (const path of unbaselined) process.stdout.write(` ${path}\n`);
61989
+ for (const path of unbaselined) process.stdout.write(` ${maskSecretsInText(path, o.secrets)}\n`);
61523
61990
  process.stdout.write(` The template does not declare these, so cdkd cannot tell an AWS-authored value from an out-of-band change and will not reset either (issue #1626). Run 'cdkd state refresh-observed ${report.stackName}' (or re-deploy) to populate observedProperties if you want them reverted too.\n`);
61524
61991
  }
61525
61992
  }
@@ -80414,7 +80881,7 @@ function createMigrateCommand() {
80414
80881
  */
80415
80882
  function buildProgram() {
80416
80883
  const program = new Command();
80417
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.32");
80884
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.33");
80418
80885
  program.hook("preAction", (_thisCommand, actionCommand) => {
80419
80886
  const { profile } = actionCommand.optsWithGlobals();
80420
80887
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;