@go-to-k/cdkd 0.283.32 → 0.283.34

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-DlEg9thJ.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-B1AQpZfU.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";
@@ -14282,27 +14282,15 @@ function hasTransitionalIndex(indexes) {
14282
14282
  * Deliberately NOT keyed on the exception NAME: AWS reports this as a plain
14283
14283
  * `ResourceInUseException`, which is the same name it uses for genuinely
14284
14284
  * terminal conflicts (deleting a table that is `CREATING`, creating one that
14285
- * already exists). Retrying on the name would turn those into a 47s stall
14286
- * before the same failure.
14285
+ * already exists). Retrying on the name would spend the WHOLE budget on them —
14286
+ * a full settle poll plus a backoff step per attempt, i.e. minutes — before
14287
+ * failing identically.
14287
14288
  */
14288
14289
  const INDEX_BUSY_DELETE_MESSAGE = /cannot delete table while indexes are being/i;
14289
14290
  function isIndexBusyDeleteError(message) {
14290
14291
  return INDEX_BUSY_DELETE_MESSAGE.test(message);
14291
14292
  }
14292
14293
  /**
14293
- * Retry budget for the index-busy `DeleteTable` refusal (issue #1830).
14294
- *
14295
- * Left at `withRetry`'s own default of 8 rather than raised. Named rather than
14296
- * inlined because passing it at all is what opts this call out of
14297
- * `withRetry`'s dense IAM-propagation schedule, which would be wrong for a
14298
- * condition measured in seconds-to-minutes rather than sub-second.
14299
- *
14300
- * The count MULTIPLIES the per-attempt re-arm poll below, so the two have to
14301
- * be read together — see {@link DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS} for the
14302
- * arithmetic and for why the poll is not a 15-minute one.
14303
- */
14304
- const DELETE_INDEX_BUSY_MAX_RETRIES = 8;
14305
- /**
14306
14294
  * `DescribeTable` polls (~1s apart) the index-busy `DeleteTable` retry spends
14307
14295
  * re-arming BEFORE each retry (issue #1830) — it returns on the first one that
14308
14296
  * reports every index settled.
@@ -14310,30 +14298,57 @@ const DELETE_INDEX_BUSY_MAX_RETRIES = 8;
14310
14298
  * Bounded well under the 15-minute default `GlobalTable`'s pre-delete #1521
14311
14299
  * gate uses, because THAT default is sized for a wait that runs once while
14312
14300
  * this one runs per retry: the caller's wall clock is
14313
- * `DELETE_INDEX_BUSY_MAX_RETRIES x this + withRetry's ~47s of backoff`, and
14314
- * `destroy-runner.ts` runs the delete under a per-resource deadline (30 min by
14315
- * default; neither DynamoDB provider declares a `getMinResourceTimeoutMs` to
14316
- * lift it). At 900 polls the product was ~2h, so a genuinely stuck index
14317
- * produced a 30-minute wait ending in a generic `ResourceTimeoutError` that
14318
- * never mentions indexes. At 60 the LOOP's worst case is ~8.8 min and the user
14319
- * gets AWS's own actionable sentence instead.
14301
+ * `its retry budget x this x ~1.2s per poll + withRetry's backoff`, and `destroy-runner.ts` runs the delete under a per-resource
14302
+ * deadline (30 min by default; neither DynamoDB provider declares a
14303
+ * `getMinResourceTimeoutMs` to lift it). At 900 polls the product would be
14304
+ * ~4.2h, so a genuinely stuck index would produce a 30-minute wait ending in a
14305
+ * generic `ResourceTimeoutError` that never mentions indexes. At 60 the LOOP's
14306
+ * worst case is ~18.4 min on `AWS::DynamoDB::Table` and ~10.4 min on
14307
+ * `AWS::DynamoDB::GlobalTable` — see {@link TABLE_DELETE_INDEX_BUSY_MAX_RETRIES}
14308
+ * for every term of that product and for why the two budgets differ — and the
14309
+ * user gets AWS's own actionable sentence instead.
14320
14310
  *
14321
14311
  * **What that deadline actually wraps**, since the arithmetic depends on it:
14322
14312
  * NOT one `delete()` but `destroy-runner.ts`'s whole outer retry loop, which
14323
14313
  * calls `delete()` up to 4 times (`maxAttempts = 3`) inside the one deadline.
14324
- * Four times ~8.8 min would blow straight through 30 min. It does not, and the
14314
+ * Four times ~18.4 min would blow straight through 30 min. It does not, and the
14325
14315
  * reason is load-bearing rather than incidental: that outer loop only retries
14326
14316
  * when `isRetryableTransientError` says so, and this refusal matches no entry
14327
14317
  * in `RETRYABLE_ERROR_MESSAGE_PATTERNS` (verified against the real classifier —
14328
14318
  * both AWS's raw sentence and the `Failed to delete DynamoDB table ...` wrap
14329
14319
  * return false), so the outer loop runs `delete()` exactly ONCE for it and the
14330
14320
  * budget below is not re-multiplied. Adding this message to those patterns
14331
- * would silently make the worst case ~35 min — over the deadline.
14321
+ * would silently make the worst case ~74 min on `AWS::DynamoDB::Table` — well
14322
+ * over the deadline.
14332
14323
  *
14333
14324
  * The FIRST attempt is unaffected — it does not re-arm at all.
14334
14325
  */
14335
14326
  const DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS = 60;
14336
14327
  /**
14328
+ * Sleep between two `DescribeTable` polls of {@link waitForIndexesSettled}.
14329
+ *
14330
+ * Named rather than inlined because it is the unit every poll budget on the
14331
+ * DynamoDB DELETE path is denominated in — a poll count only becomes a wall
14332
+ * clock through this, and the deadline arguments at
14333
+ * {@link TABLE_DELETE_INDEX_BUSY_MAX_RETRIES} multiply it. Each of those counts
14334
+ * READS it, which is what makes the claim checkable rather than a convention:
14335
+ * this module's 60-poll re-arm; `GlobalTable`'s 900-poll #1521 pre-delete gate
14336
+ * (and the same 900-poll default its auto-scaling caller takes, since both go
14337
+ * through `waitForIndexesActive` -> {@link waitForIndexesSettled});
14338
+ * `GlobalTable`'s `waitForReplicaGone` and `waitForTableGone` at 600 polls
14339
+ * each; and the `Table` provider's 60-poll `waitForTableActiveAfterUpdate`.
14340
+ *
14341
+ * Deliberately NOT claimed for the whole file: `waitForReplicaActive` keeps its
14342
+ * own `1000`, and is left alone because it runs on the CREATE / UPDATE path,
14343
+ * which no arithmetic here prices.
14344
+ *
14345
+ * It is a FLOOR on the per-poll cost, never the whole of it — each poll also
14346
+ * pays a `DescribeTable` round trip, which is why the live run's 60-poll
14347
+ * re-arm took ~72s and not 60s, and why the arithmetic above prices a poll at
14348
+ * ~1.2s.
14349
+ */
14350
+ const INDEX_SETTLE_POLL_INTERVAL_MS = 1e3;
14351
+ /**
14337
14352
  * What proceeding costs when a DELETE-path index-settle wait ends WITHOUT
14338
14353
  * confirming — passed as {@link waitForIndexesSettled}'s `proceedNote` by every
14339
14354
  * delete-path caller (`GlobalTable`'s #1521 pre-delete gate, and the #1830 /
@@ -14371,7 +14386,7 @@ async function waitForIndexesSettled(opts) {
14371
14386
  const message = err instanceof Error ? err.message : String(err);
14372
14387
  if (isThrottlingError(err)) {
14373
14388
  logger.debug(`DescribeTable throttled while waiting for indexes on ${tableName} (attempt ${attempt}/${maxAttempts}); still waiting: ${message}`);
14374
- await new Promise((resolve) => setTimeout(resolve, 1e3));
14389
+ await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
14375
14390
  continue;
14376
14391
  }
14377
14392
  if (err instanceof ResourceNotFoundException$1) {
@@ -14383,9 +14398,10 @@ async function waitForIndexesSettled(opts) {
14383
14398
  logger.warn(`DescribeTable failed (${errorName}) while waiting for indexes on ${tableName} (${logicalId}), so cdkd stopped waiting; ${proceedNote} Re-run with --verbose for AWS's own message.`);
14384
14399
  return;
14385
14400
  }
14386
- await new Promise((resolve) => setTimeout(resolve, 1e3));
14401
+ await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
14387
14402
  }
14388
- logger.warn(`Indexes on ${tableName} (${logicalId}) did not all reach ACTIVE within ${maxAttempts} DescribeTable polls (~1s apart, so a little over ${maxAttempts}s of wall clock); ${proceedNote}`);
14403
+ const pollSeconds = INDEX_SETTLE_POLL_INTERVAL_MS / 1e3;
14404
+ logger.warn(`Indexes on ${tableName} (${logicalId}) did not all reach ACTIVE within ${maxAttempts} DescribeTable polls (~${pollSeconds}s apart, so a little over ${maxAttempts * pollSeconds}s of wall clock); ${proceedNote}`);
14389
14405
  }
14390
14406
  /**
14391
14407
  * The ONE line the retry prints at default verbosity, for both providers.
@@ -14395,15 +14411,50 @@ async function waitForIndexesSettled(opts) {
14395
14411
  * and the bounded budget above can end in a timeout that has to be diagnosable
14396
14412
  * from an ordinary run's output.
14397
14413
  *
14398
- * `attemptNumber` is the attempt now STARTING (2 for the first retry). The
14399
- * remaining-attempt count is DERIVED from it and never spelled out: `withRetry`
14400
- * runs `1 + maxRetries` attempts in total, and writing the number by hand is
14401
- * how this line came to promise 8 further attempts when 7 were left.
14414
+ * `attemptNumber` is the attempt now STARTING (2 for the first retry), and
14415
+ * `maxRetries` is the CALLER's budget — the two types no longer share one (see
14416
+ * {@link TABLE_DELETE_INDEX_BUSY_MAX_RETRIES}), so a module-level constant here
14417
+ * would print the other type's promise. The remaining-attempt count is DERIVED
14418
+ * from the pair and never spelled out: `withRetry` runs `1 + maxRetries`
14419
+ * attempts in total, and writing the number by hand is how this line came to
14420
+ * promise 8 further attempts when 7 were left. Both figures are CLAMPED — the
14421
+ * attempt count at zero, the minutes at one, since the announced attempt
14422
+ * re-arms too: the function is exported, and an `attemptNumber` past the budget
14423
+ * otherwise printed "up to 0 more attempts" and then went negative.
14402
14424
  *
14403
14425
  * The per-attempt settle budget is attached to "this attempt plus the N more"
14404
14426
  * rather than to the N alone, because the re-arm runs before the attempt this
14405
- * very line announces too — 8 re-arms for 7 remaining attempts. Saying "N more
14406
- * attempts, each preceded by ..." made one sentence quote two different counts.
14427
+ * very line announces too — 14 re-arms for 13 remaining attempts on the `Table`
14428
+ * budget. Saying "N more attempts, each preceded by ..." made one sentence
14429
+ * quote two different counts.
14430
+ *
14431
+ * It also states how long the loop will keep GOING, as a floor (issue #1950).
14432
+ * At the `Table` budget the loop can run for the better part of twenty minutes,
14433
+ * and a user watching a destroy sit on one table for that long needs to tell
14434
+ * SETTLING from STUCK — an attempt count alone does not say how long it will
14435
+ * take.
14436
+ *
14437
+ * The wording is "will not give up for at least ~N more minutes", NOT "can keep
14438
+ * retrying for at least ~N". The re-arm returns on the first `DescribeTable`
14439
+ * that reports the indexes settled, so a table clearing in 90s prints this line
14440
+ * and then finishes immediately: read as a floor on the WAIT it would push the
14441
+ * user toward Ctrl-C, which is the opposite of what the line is for. It is a
14442
+ * floor on cdkd's PATIENCE, which is what the reader needs to decide whether to
14443
+ * keep waiting.
14444
+ *
14445
+ * A floor rather than an estimate because {@link INDEX_SETTLE_POLL_INTERVAL_MS}
14446
+ * is only the sleep: each poll also pays a `DescribeTable` round trip and
14447
+ * `withRetry` sleeps its backoff between attempts, so the true patience is
14448
+ * longer than the figure printed.
14449
+ *
14450
+ * The floor is conditioned on those waits RUNNING, which the sentence says out
14451
+ * loud rather than assuming. {@link waitForIndexesSettled} returns early on any
14452
+ * non-throttle `DescribeTable` failure (a describe-denied principal that can
14453
+ * still delete, say) and returns at once whenever the GSI-status proxy
14454
+ * disagrees with AWS's real refusal predicate — the gap named above. In those
14455
+ * runs every remaining retry pays only backoff, and cdkd gives up in ~1.6 min
14456
+ * having announced ~14. Unconditional, the figure would be plainly false
14457
+ * there.
14407
14458
  *
14408
14459
  * It must NOT carry AWS's own sentence: the `dynamodb-globaltable` integ greps
14409
14460
  * the destroy log for `Cannot delete table while indexes are being` to prove
@@ -14411,8 +14462,10 @@ async function waitForIndexesSettled(opts) {
14411
14462
  * the refusal having happened.
14412
14463
  */
14413
14464
  function indexBusyRetryWarning(opts) {
14414
- const remainingAttempts = 9 - opts.attemptNumber;
14415
- return `DynamoDB ${opts.typeLabel} ${opts.logicalId}: AWS refused DeleteTable on ${opts.physicalId} because a global secondary index is still being created, updated or deleted — application auto-scaling can start an index capacity change at any moment, including during this destroy. Waiting for the index to settle and retrying (this attempt plus up to ${remainingAttempts} more attempts; each one first waits up to ${60} DescribeTable polls ~1s apart — a little over a minute of settling — before its DeleteTable). Nothing is wrong with the table; a large index backfill can outlast this budget, in which case the destroy fails with AWS's own message and re-running it succeeds once the index is ACTIVE.`;
14465
+ const remainingAttempts = Math.max(0, 1 + opts.maxRetries - opts.attemptNumber);
14466
+ const settleFloorMinutes = Math.max(1, Math.round((remainingAttempts + 1) * 60 * INDEX_SETTLE_POLL_INTERVAL_MS / 6e4));
14467
+ const pollSeconds = INDEX_SETTLE_POLL_INTERVAL_MS / 1e3;
14468
+ return `DynamoDB ${opts.typeLabel} ${opts.logicalId}: AWS refused DeleteTable on ${opts.physicalId} because a global secondary index is still being created, updated or deleted — application auto-scaling can start an index capacity change at any moment, including during this destroy. Waiting for the index to settle and retrying (this attempt plus up to ${remainingAttempts} more attempts; each one first waits up to ${60} DescribeTable polls ~${pollSeconds}s apart before its DeleteTable, so cdkd will not give up for at least ~${settleFloorMinutes} more minutes while those settle waits run). Nothing is wrong with the table; a large index backfill can outlast this budget, in which case the destroy fails with AWS's own message and re-running it succeeds once the index is ACTIVE.`;
14416
14469
  }
14417
14470
  /**
14418
14471
  * Issue the caller's `DeleteTable`, retrying ONLY the transient index-busy
@@ -14430,11 +14483,19 @@ function indexBusyRetryWarning(opts) {
14430
14483
  * index backfill outlasts any fixed backoff grid, while a settle poll returns
14431
14484
  * on its first `DescribeTable` once the index is ACTIVE.
14432
14485
  *
14486
+ * `maxRetries` is the CALLER's, not this module's: the two DynamoDB types have
14487
+ * different amounts of the per-resource deadline left by the time they reach
14488
+ * this loop, so they carry different budgets (see
14489
+ * {@link TABLE_DELETE_INDEX_BUSY_MAX_RETRIES} for both derivations). Passing it
14490
+ * rather than reading a module constant is also what keeps the two providers'
14491
+ * budgets visible at their call sites.
14492
+ *
14433
14493
  * `sleepSeam` is read per SLEEP rather than once when the options bag is built,
14434
14494
  * so a test that installs the no-op from INSIDE the first refusal — i.e. after
14435
14495
  * `delete()` has already built the bag — still gets it. A value captured at
14436
14496
  * construction time would be `undefined` there and would silently pay
14437
- * `withRetry`'s real ~47s schedule. Passing `sleep` unconditionally is safe:
14497
+ * `withRetry`'s real backoff schedule (95s of sleep at the `Table` budget, 47s
14498
+ * at the `GlobalTable` one, on top of a full settle poll per attempt). Passing `sleep` unconditionally is safe:
14438
14499
  * `withRetry`'s dense-schedule detection keys on `maxRetries` /
14439
14500
  * `initialDelayMs` / `maxDelayMs` / `isRetryable`, all of which this caller
14440
14501
  * already sets.
@@ -14447,13 +14508,14 @@ async function deleteTableWithIndexBusyRetry(opts) {
14447
14508
  typeLabel: opts.typeLabel,
14448
14509
  logicalId: opts.logicalId,
14449
14510
  physicalId: opts.physicalId,
14450
- attemptNumber: deleteAttempts
14511
+ attemptNumber: deleteAttempts,
14512
+ maxRetries: opts.maxRetries
14451
14513
  }));
14452
14514
  await opts.reArm();
14453
14515
  }
14454
14516
  await opts.deleteTable();
14455
14517
  }, opts.logicalId, {
14456
- maxRetries: 8,
14518
+ maxRetries: opts.maxRetries,
14457
14519
  isRetryable: (message) => isIndexBusyDeleteError(message),
14458
14520
  logger: opts.logger,
14459
14521
  sleep: (ms) => opts.sleepSeam.sleep ? opts.sleepSeam.sleep(ms) : new Promise((resolve) => setTimeout(resolve, ms))
@@ -14992,7 +15054,8 @@ function desiredIndexEntriesByName(value) {
14992
15054
  * Test seam for the index-busy `DeleteTable` retry's backoff (issue #1931),
14993
15055
  * mirroring the sibling provider's export of the same name. Production leaves
14994
15056
  * `sleep` undefined so `withRetry`'s real schedule applies; a test injects a
14995
- * no-op so the ~47s budget does not have to be waited out.
15057
+ * no-op so the ~95s of backoff at this type's budget
15058
+ * (`TABLE_DELETE_INDEX_BUSY_MAX_RETRIES`) does not have to be waited out.
14996
15059
  *
14997
15060
  * One seam PER PROVIDER rather than one on the shared module: a `Table` test
14998
15061
  * that silences this backoff must not also silence the `GlobalTable` one, since
@@ -15210,7 +15273,7 @@ var DynamoDBTableProvider = class {
15210
15273
  }
15211
15274
  await this.dynamoDBClient.send(new UpdateTableCommand(updateInput));
15212
15275
  if (billingMode !== liveBillingMode) await this.waitForTableAndIndexesActive(physicalId, BILLING_FLIP_ACTIVE_WAIT_ATTEMPTS);
15213
- else await this.waitForTableActiveAfterUpdate(physicalId, TABLE_ACTIVE_WAIT_ATTEMPTS);
15276
+ else await this.waitForTableActiveAfterUpdate(physicalId, 60);
15214
15277
  this.logger.debug(`Updated BillingMode/ProvisionedThroughput/TableClass on DynamoDB table ${physicalId}`);
15215
15278
  }
15216
15279
  if (JSON.stringify(properties["OnDemandThroughput"]) !== JSON.stringify(previousProperties["OnDemandThroughput"])) {
@@ -15338,6 +15401,7 @@ var DynamoDBTableProvider = class {
15338
15401
  logicalId,
15339
15402
  physicalId,
15340
15403
  typeLabel: "table",
15404
+ maxRetries: 14,
15341
15405
  logger: this.logger,
15342
15406
  deleteTable: async () => {
15343
15407
  await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
@@ -15697,10 +15761,10 @@ var DynamoDBTableProvider = class {
15697
15761
  * blanket raise would turn every genuinely wedged capacity edit into a
15698
15762
  * 10-minute hang.
15699
15763
  */
15700
- async waitForTableActiveAfterUpdate(tableName, maxAttempts = TABLE_ACTIVE_WAIT_ATTEMPTS) {
15764
+ async waitForTableActiveAfterUpdate(tableName, maxAttempts = 60) {
15701
15765
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
15702
15766
  if ((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.TableStatus === "ACTIVE") return;
15703
- await new Promise((resolve) => setTimeout(resolve, 1e3));
15767
+ await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
15704
15768
  }
15705
15769
  throw new Error(`Table ${tableName} did not reach ACTIVE status within ${maxAttempts} seconds after UpdateTable`);
15706
15770
  }
@@ -17783,6 +17847,7 @@ var DynamoDBGlobalTableProvider = class {
17783
17847
  logicalId,
17784
17848
  physicalId,
17785
17849
  typeLabel: "GlobalTable",
17850
+ maxRetries: 8,
17786
17851
  logger: this.logger,
17787
17852
  deleteTable: async () => {
17788
17853
  await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
@@ -18456,10 +18521,10 @@ var DynamoDBGlobalTableProvider = class {
18456
18521
  * Delete replica update. Replica deletion typically takes 1–5 min;
18457
18522
  * cap at 10 min.
18458
18523
  */
18459
- async waitForReplicaGone(tableName, region, logicalId, maxAttempts = 600) {
18524
+ async waitForReplicaGone(tableName, region, logicalId, maxAttempts = REPLICA_GONE_WAIT_ATTEMPTS) {
18460
18525
  for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
18461
18526
  if (!(await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.Replicas?.find((r) => r.RegionName === region)) return;
18462
- await new Promise((resolve) => setTimeout(resolve, 1e3));
18527
+ await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
18463
18528
  } catch (err) {
18464
18529
  if (err instanceof ResourceNotFoundException$1) return;
18465
18530
  throw err;
@@ -18480,7 +18545,7 @@ var DynamoDBGlobalTableProvider = class {
18480
18545
  async waitForTableGone(tableName, logicalId, maxAttempts = 600) {
18481
18546
  for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
18482
18547
  await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }));
18483
- await new Promise((resolve) => setTimeout(resolve, 1e3));
18548
+ await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
18484
18549
  } catch (err) {
18485
18550
  if (err instanceof ResourceNotFoundException$1) return;
18486
18551
  throw err;
@@ -18575,10 +18640,33 @@ function autoScalingResourceId(tableName, indexName) {
18575
18640
  */
18576
18641
  const autoScalingRetryDelays = {};
18577
18642
  /**
18643
+ * Polls `waitForReplicaGone` spends waiting for a replica to disappear.
18644
+ *
18645
+ * Named rather than inlined because it is a TERM of the per-resource deadline
18646
+ * arithmetic on this type's delete path (see
18647
+ * `GLOBAL_TABLE_DELETE_INDEX_BUSY_MAX_RETRIES`), which prices it through
18648
+ * {@link INDEX_SETTLE_POLL_INTERVAL_MS} — the sleep this loop now reads rather
18649
+ * than spelling its own `1000`, so the two cannot drift while that arithmetic
18650
+ * claims they agree.
18651
+ */
18652
+ const REPLICA_GONE_WAIT_ATTEMPTS = 600;
18653
+ /**
18654
+ * Polls `waitForTableGone` spends confirming the table is actually gone.
18655
+ *
18656
+ * EXPORTED, unlike its replica sibling, because the delete-path budget fence
18657
+ * reads it: this wait runs AFTER the index-busy retry loop and only when that
18658
+ * loop SUCCEEDS, which is what keeps it out of the loop's own worst case and
18659
+ * puts it into the late-success one (both spelled out on
18660
+ * `GLOBAL_TABLE_DELETE_INDEX_BUSY_MAX_RETRIES`).
18661
+ */
18662
+ const TABLE_GONE_WAIT_ATTEMPTS = 600;
18663
+ /**
18578
18664
  * Test seam for the index-busy `DeleteTable` retry's backoff (issue #1830),
18579
18665
  * mirroring {@link autoScalingRetryDelays}. Production leaves `sleep`
18580
18666
  * undefined so `withRetry`'s real schedule applies; a test injects a no-op so
18581
- * the ~47s budget does not have to be waited out. Kept SEPARATE from the
18667
+ * the ~47s of backoff at this type's budget
18668
+ * (`GLOBAL_TABLE_DELETE_INDEX_BUSY_MAX_RETRIES`) does not have to be waited
18669
+ * out. Kept SEPARATE from the
18582
18670
  * auto-scaling seam so a test can slow one path without silencing the other.
18583
18671
  */
18584
18672
  const deleteTableRetryDelays = {};
@@ -60644,6 +60732,325 @@ function createIamPrincipalUniqueIdResolver(awsClients) {
60644
60732
  };
60645
60733
  }
60646
60734
  /**
60735
+ * Does any string anywhere in `value` carry a `{{resolve:...}}` dynamic
60736
+ * reference?
60737
+ *
60738
+ * Cheap pre-check so the overwhelming majority of resources — which reference
60739
+ * no secret at all — never build a resolver context and never pay a deep clone
60740
+ * of their property bag.
60741
+ */
60742
+ function containsDynamicReference(value) {
60743
+ if (typeof value === "string") return value.includes("{{resolve:");
60744
+ if (Array.isArray(value)) return value.some(containsDynamicReference);
60745
+ if (value !== null && typeof value === "object") return Object.values(value).some(containsDynamicReference);
60746
+ return false;
60747
+ }
60748
+ /**
60749
+ * Byte-for-byte the threshold `secret-redaction.ts` applies when it builds its
60750
+ * substring needles (`MIN_NEEDLE_LENGTH`). Copied rather than imported because
60751
+ * that constant is module-private there and this file may not widen its
60752
+ * exports; the two must agree or this function would call a leaf secret-bearing
60753
+ * that `redactSecretsForState` will not redact.
60754
+ */
60755
+ const MIN_SECRET_NEEDLE_LENGTH = 4;
60756
+ /**
60757
+ * Does `value` hold a plaintext this pass has recorded as a secret?
60758
+ *
60759
+ * Two branches with deliberately different strictness, and the asymmetry is the
60760
+ * point. A WHOLE-value match is exact evidence at any length, so it is accepted
60761
+ * unconditionally. A SUBSTRING match is only evidence when the plaintext is
60762
+ * long enough to be distinctive: a 1-3 character secret (a JSON key holding
60763
+ * `"0"`) occurs inside unrelated values constantly, and accepting it would mark
60764
+ * half the resource's paths secret-bearing — masking values that are not
60765
+ * secrets and, worse, making `--accept` refuse them.
60766
+ *
60767
+ * So this errs toward NOT marking on the substring branch, and that direction
60768
+ * is chosen rather than inherited: `redactSecretsForState` applies the same
60769
+ * threshold when it builds needles, so a sub-threshold plaintext is not
60770
+ * substituted there either. Marking a path this function's caller cannot
60771
+ * actually redact would produce a `***` with no mechanism behind it.
60772
+ */
60773
+ function carriesRecordedSecret(value, secrets) {
60774
+ if (value === "") return false;
60775
+ if (secrets.has(value)) return true;
60776
+ for (const plaintext of secrets.keys()) if (plaintext.length >= MIN_SECRET_NEEDLE_LENGTH && value.includes(plaintext)) return true;
60777
+ return false;
60778
+ }
60779
+ /**
60780
+ * Is this unresolvable token one whose value is a SECRET by definition?
60781
+ *
60782
+ * `ssm-secure` is the only such spelling, and it is decidable with no lookup:
60783
+ * CloudFormation defines exactly three dynamic-reference services (`ssm`,
60784
+ * `ssm-secure`, `secretsmanager`), cdkd resolves the other two, and
60785
+ * `ssm-secure` is the encrypted one.
60786
+ *
60787
+ * Anything else reaching the survivor arm is not a CloudFormation dynamic
60788
+ * reference at all — it is text that merely looks like one — and treating it as
60789
+ * secret is not the safe direction it appears to be: the path would be masked
60790
+ * to `***`, refused by `--accept`, and pinned to the live value by
60791
+ * {@link preserveLiveValuesAtUnresolvedTokens}, which is permanently stuck
60792
+ * drift with no remedy the user can apply. Such a token is still REPORTED; it
60793
+ * just does not claim to be a secret.
60794
+ */
60795
+ function isSecretBySpelling(token) {
60796
+ return token.startsWith("{{resolve:ssm-secure:");
60797
+ }
60798
+ /**
60799
+ * Every `{{resolve:...}}` token still present in `value`.
60800
+ *
60801
+ * Used to NAME an unresolvable reference without quoting the string it sits in.
60802
+ * That string is a partially substituted leaf: `resolveDynamicReferences`
60803
+ * substitutes token by token, so a leaf holding two references comes back with
60804
+ * the resolvable one already replaced by its PLAINTEXT. Interpolating it into a
60805
+ * log line prints the secret — from the command whose purpose is not to. A
60806
+ * token is a reference NAME and carries no value, so it is always safe to show.
60807
+ */
60808
+ function survivingDynamicReferences(value) {
60809
+ return value.match(/\{\{resolve:[^}]+\}\}/g) ?? [];
60810
+ }
60811
+ /**
60812
+ * Record the dotted path of every leaf holding a `{{resolve:...}}` string.
60813
+ *
60814
+ * The OFFLINE seed for {@link SecretPathSet}: it needs no AWS call, so it is
60815
+ * what remains when resolution fails. Issue #1900's mechanism applied to
60816
+ * positions instead of values — the record's own bags already say WHERE the
60817
+ * references are, and that is the half of the answer a failed lookup does not
60818
+ * take away.
60819
+ *
60820
+ * Necessarily coarser than the resolved answer, because a `{{resolve:ssm:...}}`
60821
+ * naming a plain `String` parameter is PUBLIC config and only the resolver's
60822
+ * TYPE verdict can say so. Marking such a path secret-bearing over-masks it,
60823
+ * which is why the seed is used ONLY on the failure path, where the alternative
60824
+ * is not masking at all.
60825
+ */
60826
+ function collectDynamicReferencePaths(value, into, path = "") {
60827
+ if (typeof value === "string") {
60828
+ if (value.includes("{{resolve:")) into.add(path);
60829
+ return;
60830
+ }
60831
+ if (Array.isArray(value)) {
60832
+ value.forEach((item, i) => collectDynamicReferencePaths(item, into, path === "" ? String(i) : `${path}.${i}`));
60833
+ return;
60834
+ }
60835
+ if (value !== null && typeof value === "object") for (const [k, v] of Object.entries(value)) collectDynamicReferencePaths(v, into, path === "" ? k : `${path}.${k}`);
60836
+ }
60837
+ /**
60838
+ * Re-resolve the SECRET dynamic references (`{{resolve:secretsmanager:...}}`,
60839
+ * and an `{{resolve:ssm:...}}` naming a `SecureString`) held by a bag read back
60840
+ * out of cdkd STATE (issue #1914).
60841
+ *
60842
+ * State stores the unresolved EXPRESSION — that is the GHSA-p5qg-v9gv-hc7w
60843
+ * redaction — while a provider's `readCurrentState` snapshot necessarily holds
60844
+ * the resolved PLAINTEXT, because that is what AWS was given at deploy time.
60845
+ * Nothing in `cdkd drift` reconciled the two, and all three of the command's
60846
+ * modes broke on it: the comparison reported permanent phantom drift and
60847
+ * printed the plaintext as the AWS-current side, `--accept` persisted that
60848
+ * plaintext back into `state.json`, and `--revert` shipped the literal
60849
+ * `{{resolve:...}}` string to the live resource.
60850
+ *
60851
+ * The counterpart of the rollback replay's `resolveReplayProps`
60852
+ * (`src/deployment/rollback-executor.ts`), and for the same reason: both
60853
+ * commands are synth-free, so the expression string in the persisted record is
60854
+ * the only thing there is to resolve from. Every plaintext is recorded into
60855
+ * `secrets` so the caller can redact it back out of anything it PRINTS or
60856
+ * PERSISTS, and every path that produced one into `secretPaths` so the caller
60857
+ * can act on a value it does NOT recognise there — see {@link redactDriftChanges}.
60858
+ *
60859
+ * A `{{resolve:...}}` token that SURVIVES the pass is reported through
60860
+ * `onUnresolved` and left in place — it is not an error. The resolver's
60861
+ * unsupported-service arm (`ssm-secure:` is the live example) warns and returns
60862
+ * the literal, and `cdkd deploy` resolves through the very same code, so AWS is
60863
+ * ALREADY holding that literal string and state records it. Replaying it is
60864
+ * therefore a correct no-op, and failing the resource over it would take every
60865
+ * other drifted property on that resource down with it (plus exit 2). The
60866
+ * report exists so the user learns cdkd is shipping a token it cannot resolve,
60867
+ * which is a real thing to know and not a reason to stop.
60868
+ *
60869
+ * Only the TOKENS are reported, never the leaf that held them: the leaf is
60870
+ * partially substituted by this point, so a mixed `secretsmanager` +
60871
+ * `ssm-secure` string comes back carrying real plaintext.
60872
+ *
60873
+ * Returns the input by identity when it holds no dynamic reference, so the
60874
+ * non-secret path is unchanged down to object identity.
60875
+ */
60876
+ async function resolveStateSecretExpressions(props, resolver, secrets, options = {}) {
60877
+ if (!containsDynamicReference(props)) return props;
60878
+ const { secretPaths, onUnresolved } = options;
60879
+ const ctx = {
60880
+ template: { Resources: {} },
60881
+ resources: {},
60882
+ recordedSecretValues: secrets
60883
+ };
60884
+ const walk = async (v, path) => {
60885
+ if (typeof v === "string") {
60886
+ if (!v.includes("{{resolve:")) return v;
60887
+ const resolved = await resolver.resolveDynamicReferences(v, ctx);
60888
+ const survivors = survivingDynamicReferences(resolved).filter((t) => v.includes(t));
60889
+ if (survivors.length > 0) {
60890
+ if (survivors.some(isSecretBySpelling)) secretPaths?.add(path);
60891
+ onUnresolved?.(survivors, path);
60892
+ }
60893
+ if (carriesRecordedSecret(resolved, secrets)) secretPaths?.add(path);
60894
+ return resolved;
60895
+ }
60896
+ if (Array.isArray(v)) {
60897
+ const out = new Array(v.length);
60898
+ for (let i = 0; i < v.length; i++) out[i] = await walk(v[i], path === "" ? String(i) : `${path}.${i}`);
60899
+ return out;
60900
+ }
60901
+ if (v !== null && typeof v === "object") {
60902
+ const out = {};
60903
+ for (const [k, val] of Object.entries(v)) out[k] = await walk(val, path === "" ? k : `${path}.${k}`);
60904
+ return out;
60905
+ }
60906
+ return v;
60907
+ };
60908
+ return await walk(props, "");
60909
+ }
60910
+ /**
60911
+ * Is a drift reported at `path` positioned on, or above, a known secret?
60912
+ *
60913
+ * The PREFIX direction is the one that matters: a secret sits at a leaf (or at
60914
+ * an array the comparator never descends), while a drift can be reported at any
60915
+ * ancestor of it — `Environment` rather than
60916
+ * `Environment.Variables.SECRET_PASSWORD` — whenever the two sides disagree
60917
+ * about the shape rather than the value. The reverse containment is checked too
60918
+ * and costs nothing.
60919
+ */
60920
+ function isSecretBearingPath(path, secretPaths) {
60921
+ if (secretPaths.size === 0) return false;
60922
+ for (const secretPath of secretPaths) if (secretPath === path || secretPath.startsWith(`${path}.`) || path.startsWith(`${secretPath}.`)) return true;
60923
+ return false;
60924
+ }
60925
+ /**
60926
+ * Redact one side of a drift entry.
60927
+ *
60928
+ * Two passes, and the second is the one issue #1914's review turned up. The
60929
+ * VALUE pass rewrites any recorded plaintext back onto its own
60930
+ * `{{resolve:...}}` expression, which is exact and is all that is needed while
60931
+ * the secret AWS holds is the secret the reference resolves to today.
60932
+ *
60933
+ * It stops being enough the moment those two differ, and they differ for the
60934
+ * most ordinary reason there is: a Secrets Manager ROTATION. The deployed
60935
+ * resource then still holds the PREVIOUS secret while the re-resolved baseline
60936
+ * holds the new one, so the path drifts and the AWS side matches no key in a
60937
+ * map built from today's value — it is a real secret that would be printed
60938
+ * verbatim and persisted by `--accept`. An out-of-band edit is
60939
+ * indistinguishable from it at this layer, so the POSITION decides: at a path
60940
+ * known to carry a secret, the only value ever shown is the expression itself,
60941
+ * and anything else becomes {@link SECRET_MASK}.
60942
+ *
60943
+ * `undefined` and `null` are exempt — an absent or null key discloses nothing,
60944
+ * and masking them would turn "AWS does not have this key" into "AWS has
60945
+ * something secret here", which is both wrong and less useful.
60946
+ */
60947
+ function redactDriftValue(value, secrets, secretBearing) {
60948
+ const redacted = redactSecretsForState(value, secrets);
60949
+ if (!secretBearing) return redacted;
60950
+ if (redacted === void 0 || redacted === null) return redacted;
60951
+ if (typeof redacted === "string" && isWholeDynamicReference(redacted)) return redacted;
60952
+ return "***";
60953
+ }
60954
+ /**
60955
+ * Is the WHOLE string a single `{{resolve:...}}` token and nothing else?
60956
+ *
60957
+ * Byte-for-byte `secret-redaction.ts`'s `isSingleDynamicReferenceToken`,
60958
+ * including the `[^{}]*` character class. The difference from `[^}]+` is
60959
+ * narrow and worth stating correctly: it is a `{` INSIDE the token, e.g.
60960
+ * `{{resolve:ssm:/a{b}}`. (A previous version of this comment claimed
60961
+ * `{{resolve:a}}{{resolve:b}}` would pass under `[^}]+` — it would not, since
60962
+ * `[^}]+` cannot span the `}` in the middle either.) Matching the sibling
60963
+ * exactly is what matters here: this predicate decides whether a value may be
60964
+ * shown INSTEAD of `SECRET_MASK`, and `redactByPath` uses its copy to decide
60965
+ * whether a leaf is already an expression, so a disagreement would let one of
60966
+ * them treat as a token what the other treats as data. Copied rather than
60967
+ * imported because that helper is module-private and this file may not widen
60968
+ * that module's exports.
60969
+ */
60970
+ function isWholeDynamicReference(value) {
60971
+ return /^\{\{resolve:[^{}]*\}\}$/.test(value);
60972
+ }
60973
+ /**
60974
+ * Why `--accept` must not persist this drift, or `undefined` when it may.
60975
+ *
60976
+ * Shared by the WRITE (`runAccept`) and the PLAN (`printAcceptPlan`) so the two
60977
+ * cannot disagree — a `--dry-run` that promises a write the real run refuses is
60978
+ * worse than either behaviour on its own.
60979
+ *
60980
+ * Two refusals, both about a value cdkd could not identify rather than about
60981
+ * secrecy as such:
60982
+ *
60983
+ * - A masked VALUE says AWS holds something at a known-secret path that is not
60984
+ * what the reference resolves to. Writing `***` would corrupt the baseline
60985
+ * and make the next deploy push the literal mask at AWS.
60986
+ * - A masked PATH says the readback answered with a map KEYED by a secret.
60987
+ * `setAtPath` would then create a key literally named `***` — and when
60988
+ * `awsValue` is `undefined` it would INSERT that key rather than removing the
60989
+ * real one, so the value check alone does not cover it.
60990
+ *
60991
+ * The path check is what makes the value exemptions safe: `redactDriftValue`
60992
+ * lets `undefined` / `null` / a whole expression through unmasked, which is
60993
+ * right for a value and says nothing about the path it sits at.
60994
+ */
60995
+ function acceptRefusalReason(change, maskedPaths) {
60996
+ if (!maskedPaths.has(change.path)) return void 0;
60997
+ 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";
60998
+ 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";
60999
+ return "it carries a secret dynamic reference and the value AWS currently holds is not the one the reference resolves to";
61000
+ }
61001
+ /**
61002
+ * Redact resolved secret plaintext out of the drift entries that get REPORTED
61003
+ * (issue #1914).
61004
+ *
61005
+ * Both sides need it, for different reasons. `stateValue` comes from the
61006
+ * re-resolved comparison baseline, so at a secret leaf it now holds the
61007
+ * plaintext the state record deliberately does not store. `awsValue` is the
61008
+ * live readback, which holds that plaintext by construction. Either one
61009
+ * reaching `writeHumanReport` / `writeJsonReport` / the `--accept` and
61010
+ * `--revert` plans is the disclosure the redaction exists to prevent.
61011
+ *
61012
+ * Redacting `awsValue` also settles what `--accept` PERSISTS: that is the value
61013
+ * it writes into the baseline, so a secret one arrives at the state write
61014
+ * already carrying its expression — or, when it could not be recognised, as
61015
+ * {@link SECRET_MASK}, which `runAccept` refuses to persist.
61016
+ *
61017
+ * The PATH is redacted too. `redactSecretsForState` walks values and never
61018
+ * object KEYS, so a readback that answers with a map keyed by a secret (a
61019
+ * Lambda env var NAMED with one) renders as
61020
+ * `Environment.Variables.<plaintext>` in every printer. It is masked here
61021
+ * rather than assumed impossible, and a masked path also marks the change
61022
+ * secret-bearing, so `--accept` will not write a key it can no longer name.
61023
+ *
61024
+ * Returns the input array by identity when there is nothing to act on.
61025
+ */
61026
+ function redactDriftChanges(changes, secrets, secretPaths) {
61027
+ if (secrets.size === 0 && secretPaths.size === 0) return {
61028
+ changes,
61029
+ maskedPaths: /* @__PURE__ */ new Set()
61030
+ };
61031
+ const maskedPaths = /* @__PURE__ */ new Set();
61032
+ const redacted = [];
61033
+ for (const change of changes) {
61034
+ const maskedPath = maskSecretsInText(change.path, secrets);
61035
+ const positionIsSecret = isSecretBearingPath(change.path, secretPaths);
61036
+ const nameCarriesSecret = maskedPath !== change.path;
61037
+ const secretBearing = positionIsSecret || nameCarriesSecret;
61038
+ if (secretPaths.has(change.path) && change.awsValue === void 0) continue;
61039
+ const stateValue = redactDriftValue(change.stateValue, secrets, secretBearing);
61040
+ const awsValue = redactDriftValue(change.awsValue, secrets, secretBearing);
61041
+ if (maskedPath !== change.path || secretBearing && awsValue === "***" || secretBearing && change.awsValue === void 0) maskedPaths.add(maskedPath);
61042
+ redacted.push({
61043
+ path: maskedPath,
61044
+ stateValue,
61045
+ awsValue
61046
+ });
61047
+ }
61048
+ return {
61049
+ changes: redacted,
61050
+ maskedPaths
61051
+ };
61052
+ }
61053
+ /**
60647
61054
  * Run drift detection for one stack and shape the per-resource outcomes
60648
61055
  * into a {@link StackDriftReport}. The state object + etag are stored on
60649
61056
  * the report so `--accept` can write back without a re-read, and
@@ -60656,6 +61063,8 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
60656
61063
  return await withStackName(stackName, async () => {
60657
61064
  const outcomes = [];
60658
61065
  const state = result.state;
61066
+ const logger = getLogger();
61067
+ const secretResolver = new IntrinsicFunctionResolver(region);
60659
61068
  const entries = Object.entries(state.resources ?? {}).sort(([a], [b]) => a.localeCompare(b));
60660
61069
  for (const [logicalId, resource] of entries) {
60661
61070
  if (providerRegistry.shouldSkipResource(resource.resourceType)) continue;
@@ -60715,19 +61124,46 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
60715
61124
  const unorderedPaths = provider.getDriftUnorderedPaths ? provider.getDriftUnorderedPaths(resource.resourceType) : [];
60716
61125
  const useObserved = resource.observedProperties !== void 0;
60717
61126
  const baseline = useObserved ? resource.observedProperties : resource.properties ?? {};
61127
+ const secrets = /* @__PURE__ */ new Map();
61128
+ const secretPaths = /* @__PURE__ */ new Set();
61129
+ const seededSecretPaths = /* @__PURE__ */ new Set();
61130
+ collectDynamicReferencePaths(baseline, seededSecretPaths);
61131
+ if (useObserved) collectDynamicReferencePaths(resource.properties ?? {}, seededSecretPaths);
61132
+ const unresolvedTokens = /* @__PURE__ */ new Set();
61133
+ const noteUnresolved = (tokens) => {
61134
+ for (const token of tokens) unresolvedTokens.add(token);
61135
+ };
61136
+ let comparisonBaseline = baseline;
61137
+ let secretResolutionFailed = false;
61138
+ try {
61139
+ comparisonBaseline = await resolveStateSecretExpressions(baseline, secretResolver, secrets, {
61140
+ secretPaths,
61141
+ onUnresolved: noteUnresolved
61142
+ });
61143
+ if (useObserved) await resolveStateSecretExpressions(resource.properties ?? {}, secretResolver, secrets, {
61144
+ secretPaths,
61145
+ onUnresolved: noteUnresolved
61146
+ });
61147
+ } catch (err) {
61148
+ secretResolutionFailed = true;
61149
+ comparisonBaseline = baseline;
61150
+ 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)}`);
61151
+ secrets.clear();
61152
+ }
61153
+ 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
61154
  const observedIgnorePaths = useObserved ? undeclaredEmptyObservedKeys(resource.observedProperties, resource.properties ?? {}) : [];
60719
- const normalized = await canonicalizePrincipalUniqueIds(baseline, aws, resolvePrincipalUniqueId);
61155
+ const normalized = await canonicalizePrincipalUniqueIds(comparisonBaseline, aws, resolvePrincipalUniqueId);
60720
61156
  const protocolNormalized = canonicalizeIpProtocols(normalized.baseline, normalized.aws, resource.resourceType);
60721
61157
  const canonicalized = provider.canonicalizeDriftProperties ? {
60722
61158
  baseline: provider.canonicalizeDriftProperties(resource.resourceType, protocolNormalized.baseline),
60723
61159
  aws: provider.canonicalizeDriftProperties(resource.resourceType, protocolNormalized.aws)
60724
61160
  } : protocolNormalized;
60725
- const changes = calculateResourceDrift(canonicalized.baseline, canonicalized.aws, {
61161
+ const reported = redactDriftChanges(calculateResourceDrift(canonicalized.baseline, canonicalized.aws, {
60726
61162
  ignorePaths: observedIgnorePaths.length ? [...ignorePaths, ...observedIgnorePaths] : ignorePaths,
60727
61163
  unionWalkObjects: useObserved,
60728
61164
  unorderedPaths
60729
- });
60730
- if (changes.length === 0) outcomes.push({
61165
+ }), secrets, secretResolutionFailed ? seededSecretPaths : secretPaths);
61166
+ if (reported.changes.length === 0) outcomes.push({
60731
61167
  kind: "clean",
60732
61168
  logicalId,
60733
61169
  resourceType: resource.resourceType
@@ -60736,8 +61172,10 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
60736
61172
  kind: "drifted",
60737
61173
  logicalId,
60738
61174
  resourceType: resource.resourceType,
60739
- changes,
60740
- awsProperties: aws
61175
+ ...reported,
61176
+ awsProperties: aws,
61177
+ secrets,
61178
+ referencesUnresolved: secretResolutionFailed || unresolvedTokens.size > 0
60741
61179
  });
60742
61180
  }
60743
61181
  return {
@@ -60779,6 +61217,20 @@ function buildReadCurrentStateContext(state, excludedLogicalId) {
60779
61217
  }
60780
61218
  return { siblings };
60781
61219
  }
61220
+ /**
61221
+ * Read the value at a dotted path, or `undefined` when any segment is missing.
61222
+ * The read counterpart of {@link setAtPath}, and it parses the same subset:
61223
+ * the drift comparator only synthesizes paths through plain objects.
61224
+ */
61225
+ function getAtPath(source, path) {
61226
+ if (path.length === 0) return source;
61227
+ let cursor = source;
61228
+ for (const segment of path.split(".")) {
61229
+ if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) return void 0;
61230
+ cursor = cursor[segment];
61231
+ }
61232
+ return cursor;
61233
+ }
60782
61234
  function setAtPath(target, path, value) {
60783
61235
  if (path.length === 0) return;
60784
61236
  const segments = path.split(".");
@@ -60831,13 +61283,27 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
60831
61283
  const hasObserved = existing.observedProperties !== void 0;
60832
61284
  const baselineSource = hasObserved ? existing.observedProperties : existing.properties ?? {};
60833
61285
  const newBaseline = JSON.parse(JSON.stringify(baselineSource));
60834
- for (const change of outcome.changes) setAtPath(newBaseline, change.path, change.awsValue);
61286
+ const accepted = [];
61287
+ for (const change of outcome.changes) {
61288
+ const refusal = acceptRefusalReason(change, outcome.maskedPaths);
61289
+ if (refusal !== void 0) {
61290
+ 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.`);
61291
+ continue;
61292
+ }
61293
+ setAtPath(newBaseline, change.path, change.awsValue);
61294
+ accepted.push(change);
61295
+ }
61296
+ const redactedBaseline = redactSecretsForState(newBaseline, outcome.secrets, existing.properties ?? {}, STATE_SOURCED_READBACK_RULES);
61297
+ for (const change of accepted) {
61298
+ if (deepEqualUnordered(getAtPath(redactedBaseline, change.path), change.awsValue)) continue;
61299
+ 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.`);
61300
+ }
60835
61301
  resources[outcome.logicalId] = hasObserved ? {
60836
61302
  ...existing,
60837
- observedProperties: newBaseline
61303
+ observedProperties: redactedBaseline
60838
61304
  } : {
60839
61305
  ...existing,
60840
- properties: newBaseline
61306
+ properties: redactedBaseline
60841
61307
  };
60842
61308
  }
60843
61309
  const newState = {
@@ -61198,6 +61664,57 @@ function mergeUntemplatedValue(awsValue, desiredValue) {
61198
61664
  return merged;
61199
61665
  }
61200
61666
  /**
61667
+ * Replace every leaf of a revert payload that still holds a `{{resolve:...}}`
61668
+ * token with the value AWS currently has at that position (issue #1914,
61669
+ * round-3 review).
61670
+ *
61671
+ * The round-3 change downgraded an unresolvable reference from a per-resource
61672
+ * failure to a warning, on the premise that `cdkd deploy` resolves through the
61673
+ * resolver's same unsupported-service arm — so AWS already holds the literal
61674
+ * and replaying it is a no-op. **That premise holds only for records cdkd
61675
+ * deployed.** CloudFormation resolves `ssm-secure` SERVER-side, so a record
61676
+ * adopted by `cdkd import --migrate-from-cloudformation` has the literal token
61677
+ * in state while the live resource holds the PLAINTEXT. `diffAt` skips such a
61678
+ * leaf, so it never drifts — but `buildRevertNewProperties` overlays the whole
61679
+ * top-level subtree when any SIBLING key drifts, which would push the literal
61680
+ * token over a resolved value. That is defect 1 of this issue surviving in a
61681
+ * narrower case, on a live AWS write.
61682
+ *
61683
+ * Deciding by PROVENANCE would need a flag state does not carry. Deciding by
61684
+ * what AWS actually holds needs nothing and is exact in both directions: for a
61685
+ * cdkd-deployed record AWS holds the token, so copying it back is the same
61686
+ * no-op the premise described; for a migrated record AWS holds the resolved
61687
+ * value, which is left untouched.
61688
+ *
61689
+ * Falls back to KEEPING the token wherever AWS has nothing at that position —
61690
+ * that is what `cdkd deploy` sends, so it is the safe residual rather than
61691
+ * dropping a property the resource may require. Array descent is positional
61692
+ * and bails to that same residual on any length mismatch, since a reordered
61693
+ * readback cannot be positioned against.
61694
+ */
61695
+ function preserveLiveValuesAtUnresolvedTokens(send, awsProperties, secrets) {
61696
+ const walk = (value, live) => {
61697
+ if (typeof value === "string" && value.includes("{{resolve:")) {
61698
+ if (!isWholeDynamicReference(value)) return value;
61699
+ if (live === void 0) return value;
61700
+ if (typeof live === "string" && live.length >= MIN_SECRET_NEEDLE_LENGTH && isSecretBySpelling(value)) secrets.set(live, value);
61701
+ return live;
61702
+ }
61703
+ if (Array.isArray(value)) {
61704
+ const liveItems = Array.isArray(live) && live.length === value.length ? live : void 0;
61705
+ return value.map((item, i) => walk(item, liveItems?.[i]));
61706
+ }
61707
+ if (value !== null && typeof value === "object") {
61708
+ const liveObject = isPlainRecord(live) ? live : void 0;
61709
+ const out = {};
61710
+ for (const [k, v] of Object.entries(value)) out[k] = walk(v, liveObject?.[k]);
61711
+ return out;
61712
+ }
61713
+ return value;
61714
+ };
61715
+ return walk(send, awsProperties);
61716
+ }
61717
+ /**
61201
61718
  * Build the `newProperties` object passed to `provider.update` during
61202
61719
  * `--revert`. Strategy:
61203
61720
  *
@@ -61371,11 +61888,13 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
61371
61888
  let totalFailed = 0;
61372
61889
  let totalUnsupported = 0;
61373
61890
  let totalSucceeded = 0;
61891
+ let totalUnresolvable = 0;
61374
61892
  for (const report of reports) {
61375
61893
  const driftedOutcomes = report.outcomes.filter((o) => o.kind === "drifted");
61376
61894
  if (driftedOutcomes.length === 0) continue;
61377
61895
  await lockManager.acquireLock(report.stackName, report.region, owner, "drift-revert");
61378
61896
  const narrowedByLogicalId = /* @__PURE__ */ new Map();
61897
+ const revertSecretResolver = new IntrinsicFunctionResolver(report.region);
61379
61898
  try {
61380
61899
  await runWithConcurrency(driftedOutcomes.map((outcome) => async () => {
61381
61900
  const stateResource = report.state.resources[outcome.logicalId];
@@ -61388,28 +61907,53 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
61388
61907
  resourceType: outcome.resourceType,
61389
61908
  provisionedBy: stateResource.provisionedBy
61390
61909
  }).provider;
61391
- const desiredProperties = stateResource.observedProperties ?? stateResource.properties ?? {};
61392
- const newProperties = buildRevertNewProperties(outcome.changes, desiredProperties, outcome.awsProperties, { preserveUntemplated: stateResource.observedProperties === void 0 });
61910
+ const secrets = /* @__PURE__ */ new Map();
61911
+ const revertBaseline = stateResource.observedProperties ?? stateResource.properties ?? {};
61912
+ const unresolvedTokens = /* @__PURE__ */ new Set();
61913
+ const noteUnresolved = (tokens) => {
61914
+ for (const token of tokens) unresolvedTokens.add(token);
61915
+ };
61916
+ let desiredProperties;
61917
+ try {
61918
+ desiredProperties = await resolveStateSecretExpressions(revertBaseline, revertSecretResolver, secrets, { onUnresolved: noteUnresolved });
61919
+ if (stateResource.observedProperties !== void 0) await resolveStateSecretExpressions(stateResource.properties ?? {}, revertSecretResolver, secrets, { onUnresolved: noteUnresolved });
61920
+ } catch (err) {
61921
+ totalUnresolvable++;
61922
+ 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)}`);
61923
+ return;
61924
+ }
61925
+ const unrevertablePaths = outcome.changes.map((c) => c.path).filter((path) => (path.split(".", 1)[0] ?? "").includes("***"));
61926
+ 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.`);
61927
+ 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.`);
61928
+ let newProperties;
61929
+ try {
61930
+ const overlaid = buildRevertNewProperties(outcome.changes, desiredProperties, outcome.awsProperties, { preserveUntemplated: stateResource.observedProperties === void 0 });
61931
+ newProperties = unresolvedTokens.size > 0 ? preserveLiveValuesAtUnresolvedTokens(overlaid, outcome.awsProperties, secrets) : overlaid;
61932
+ } catch (err) {
61933
+ totalFailed++;
61934
+ logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not build the revert payload — ${maskSecretsInText(err instanceof Error ? err.message : String(err), secrets)}`);
61935
+ return;
61936
+ }
61393
61937
  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) } });
61938
+ 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
61939
  totalSucceeded++;
61396
61940
  logger.info(` ✓ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): reverted.`);
61397
61941
  try {
61398
61942
  if (updateResult?.effectiveProperties) {
61399
61943
  const delta = collectNarrowedTopLevelKeys(newProperties, updateResult.effectiveProperties);
61400
- if (Object.keys(delta).length > 0) narrowedByLogicalId.set(outcome.logicalId, delta);
61944
+ if (Object.keys(delta).length > 0) narrowedByLogicalId.set(outcome.logicalId, redactSecretsForState(delta, secrets, revertBaseline, STATE_SOURCED_READBACK_RULES));
61401
61945
  }
61402
61946
  } 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)}`);
61947
+ 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
61948
  }
61405
61949
  } catch (err) {
61406
61950
  if (err instanceof ResourceUpdateNotSupportedError) {
61407
61951
  totalUnsupported++;
61408
- logger.warn(` ⊘ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not revert — ${err.message}`);
61952
+ logger.warn(` ⊘ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not revert — ${maskSecretsInText(err.message, secrets)}`);
61409
61953
  return;
61410
61954
  }
61411
61955
  totalFailed++;
61412
- const msg = err instanceof Error ? err.message : String(err);
61956
+ const msg = maskSecretsInText(err instanceof Error ? err.message : String(err), secrets);
61413
61957
  logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): AWS update failed — ${msg}`);
61414
61958
  }
61415
61959
  }), concurrency);
@@ -61467,10 +62011,11 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
61467
62011
  }
61468
62012
  const summaryParts = [`${totalSucceeded} reverted`];
61469
62013
  if (totalUnsupported > 0) summaryParts.push(`${totalUnsupported} update-not-supported`);
62014
+ if (totalUnresolvable > 0) summaryParts.push(`${totalUnresolvable} reference-unresolvable`);
61470
62015
  if (totalFailed > 0) summaryParts.push(`${totalFailed} failed`);
61471
62016
  logger.info(`\nRevert summary: ${summaryParts.join(", ")}.`);
61472
62017
  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.`);
62018
+ 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
62019
  }
61475
62020
  /**
61476
62021
  * Print the planned state mutations for `--accept` (no AWS calls). One
@@ -61485,7 +62030,14 @@ function printAcceptPlan(reports) {
61485
62030
  process.stdout.write(`\nPlan (--accept): update cdkd state for ${report.stackName} (${report.region}):\n`);
61486
62031
  for (const o of drifted) {
61487
62032
  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`);
62033
+ for (const change of o.changes) {
62034
+ const refusal = acceptRefusalReason(change, o.maskedPaths);
62035
+ if (refusal !== void 0) {
62036
+ process.stdout.write(` ${change.path}: SKIPPED — ${refusal}\n`);
62037
+ continue;
62038
+ }
62039
+ process.stdout.write(` ${change.path}: ${formatScalar(change.stateValue)} -> ${formatScalar(change.awsValue)}\n`);
62040
+ }
61489
62041
  }
61490
62042
  }
61491
62043
  }
@@ -61506,20 +62058,23 @@ function printRevertPlan(reports) {
61506
62058
  const stateResource = report.state.resources[o.logicalId];
61507
62059
  if (stateResource) {
61508
62060
  const preserved = findRevertPreservedTagKeys(o.changes, stateResource.observedProperties ?? stateResource.properties ?? {}, o.awsProperties);
61509
- if (preserved.length > 0) {
62061
+ const cannotMaskKeys = o.referencesUnresolved;
62062
+ 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`);
62063
+ if (!cannotMaskKeys && preserved.length > 0) {
61510
62064
  const tagWord = preserved.length === 1 ? "tag" : "tags";
61511
62065
  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`);
62066
+ for (const path of preserved) process.stdout.write(` ${maskSecretsInText(path, o.secrets)}\n`);
61513
62067
  const othersRevert = stateResource.observedProperties === void 0 ? "" : `Every other tag reverts normally. `;
61514
62068
  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
62069
  }
61516
62070
  }
61517
62071
  if (stateResource && stateResource.observedProperties === void 0) {
61518
62072
  const unbaselined = findRevertUnbaselinedAwsKeys(o.changes, stateResource.properties ?? {}, o.awsProperties);
61519
- if (unbaselined.length > 0) {
62073
+ 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`);
62074
+ else if (unbaselined.length > 0) {
61520
62075
  const word = unbaselined.length === 1 ? "value" : "values";
61521
62076
  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`);
62077
+ for (const path of unbaselined) process.stdout.write(` ${maskSecretsInText(path, o.secrets)}\n`);
61523
62078
  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
62079
  }
61525
62080
  }
@@ -80414,7 +80969,7 @@ function createMigrateCommand() {
80414
80969
  */
80415
80970
  function buildProgram() {
80416
80971
  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");
80972
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.34");
80418
80973
  program.hook("preAction", (_thisCommand, actionCommand) => {
80419
80974
  const { profile } = actionCommand.optsWithGlobals();
80420
80975
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;