@go-to-k/cdkd 0.284.13 → 0.284.15

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 isTerminationProtectionPropagationError, $n as ResourceUpdateNotSupportedError, $t as validateAssetBucketName, A as renderStatefulReason, An as derivePartitionAndUrlSuffix, At as INTRINSIC_KEYS, B as exportAliasCollisionScrubWarning, Bn as CdkdError, Bt as stringifyValue, C as isFinalSnapshotError, Cn as CFN_TEMPLATE_URL_LIMIT, Ct as s3BucketArn, D as extractDeploymentEventError, Dn as expectedOwnerParam, Dt as s3BucketWebsiteUrl, E as makeCanonicalizePropertiesFn, En as uploadCfnTemplate, Et as s3BucketRegionalDomainName, F as green, Fn as AwsClients, Ft as LockManager, G as collectInlinePolicyNamesManagedBySiblings, Gn as LocalMigrateError, Gt as rewriteTemplateAssetReferences, H as secretBearingStateKeyWarning, Ht as buildAssetRedirectMap, I as red, In as getAwsClients, It as S3StateBackend, J as findActionableSilentDrops, Jn as MissingCdkCliError, Jt as AssetModeResolver, K as clearOnUpdateRemoval, Kn as LocalStartServiceError, Kt as escapeRegExp, L as yellow, Ln as resetAwsClients, Lt as rebuildClientForBucketRegion, M as bold, Mn as processStackMessages, Mt as withRetry, N as cyan, Nt as DagBuilder, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as PARTITION_TABLE, Ot as applyRoleArnIfSet, P as gray, Pn as resolveBucketRegion, Pt as TemplateParser, Q as disableInstanceApiTermination, Qn as ResourceTimeoutError, Qt as parseBootstrapMarker, R as collectDeclaredOutputNames, Rn as setAwsClients, Rt as shouldRetainResource, S as createPreDeleteFinalSnapshot, Sn as CFN_TEMPLATE_BODY_LIMIT, St as scrubResourceRecord, T as unsupportedFinalSnapshotError, Tn as findLargeInlineResources, Tt as s3BucketDualStackDomainName, U as stateKeySecretExposure, Un as DeployCancelledError, Ut as createAssetRedirectResolver, V as isExportAliasCollision, Vt as WorkGraph, W as IAMRoleProvider, Wn as LocalInvokeBuildError$1, Wt as loadPublishableAssetManifest, X as CloudControlProvider, Xn as PartialFailureError, Xt as ensureAssetStorage, Y as findSilentDropProperties, Yn as NestedStackChildDirectDestroyError, Yt as BOOTSTRAP_MARKER_PREFIX, Z as slowCcOperationTimeoutMs, Zn as ProvisioningError, Zt as getBootstrapMarkerKey, _ as computeImplicitDeleteEdges, _n as resolveStateBucketWithDefault, _t as STATE_SOURCED_READBACK_RULES, a as DeploymentEventsStore, an as runDockerForeground, at as normalizeAwsTagsToCfn, b as buildFinalSnapshotIdentifier, bn as stateBucketExistenceConfirmed, bt as maskSecretsInText, c as replayFailedOperations, cn as getDockerImageBySourceHash, cr as isMarkedNonRetryable, ct as coerceCfnBoolean, d as updatePartialReason, dn as getDefaultStateBucketName, dr as markNonRetryable, dt as readConfigString, en as validateContainerRepoName, er as StackHasActiveImportsError, et as IntrinsicFunctionResolver, f as UNSPECIFIED_SKIP_REASON, fn as getLegacyStateBucketName, ft as replayWarn, g as IMPLICIT_DELETE_DEPENDENCIES, gn as resolveSkipPrefix, gt as STATE_SOURCED_CROSS_GENERATION_RULES, h as maskingRetryLogger, hn as resolveCaptureObservedState, ht as requireConfigString, i as DeploymentEventsReader, in as getDockerCmd, it as WAFv2WebACLProvider, j as formatResourceLine, jn as AssemblyReader, jt as describeTypeWithThrottleRetry, k as isStatefulRecreateTargetSync, kn as canonicalizeRegion, kt as DiffCalculator, l as replayRollback, ln as Synthesizer, lr as isRetryableTransientError, lt as configBooleanRefusal, m as withResourceDeadline, mn as resolveAutoAssetStorage, mt as requireConfigObject, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as buildDockerImage, nt as getAccountInfo, o as planFailedOps, on as runDockerStreaming, or as normalizeAwsError, ot as resolveExplicitPhysicalId, p as deleteSkipReason, pn as resolveApp, pt as requireConfigArray, q as ProviderRegistry, qt as stripControlChars, r as DeployEngine, rn as formatDockerLoginError, rt as refStateLookupFromResource, s as planRollback, sn as AssetManifestLoader, sr as withErrorHandling, st as assertRegionMatch, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as buildDenyExternalAccessPolicy, tr as StackTerminationProtectionError, tt as cfnRefValueFromPhysicalId, u as updatePartialMessage, un as synthesisStatusMessage, ur as isThrottlingError, ut as configStringRefusal, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as resolveStateBucketWithDefaultAndSource, vt as TEMPLATE_SOURCED_RULES, w as refusesFinalSnapshot, wn as MIGRATE_TMP_PREFIX, wt as s3BucketDomainName, x as ccRoutedFinalSnapshotError, xn as warnDeprecatedNoPrefixCliFlag, xt as redactSecretsForState, y as PRE_DELETE_SNAPSHOT_TYPES, yn as resolveUseCdkBootstrapAssets, yt as createSecretMasker, z as collectPublishedOutputNames, zt as AssetPublisher } from "./deploy-engine-4Eh5Qqlm.js";
2
+ import { $ as isTerminationProtectionPropagationError, $n as ResourceUpdateNotSupportedError, $t as validateAssetBucketName, A as renderStatefulReason, An as derivePartitionAndUrlSuffix, At as INTRINSIC_KEYS, B as exportAliasCollisionScrubWarning, Bn as CdkdError, Bt as stringifyValue, C as isFinalSnapshotError, Cn as CFN_TEMPLATE_URL_LIMIT, Ct as s3BucketArn, D as extractDeploymentEventError, Dn as expectedOwnerParam, Dt as s3BucketWebsiteUrl, E as makeCanonicalizePropertiesFn, En as uploadCfnTemplate, Et as s3BucketRegionalDomainName, F as green, Fn as AwsClients, Ft as LockManager, G as collectInlinePolicyNamesManagedBySiblings, Gn as LocalMigrateError, Gt as rewriteTemplateAssetReferences, H as secretBearingStateKeyWarning, Ht as buildAssetRedirectMap, I as red, In as getAwsClients, It as S3StateBackend, J as findActionableSilentDrops, Jn as MissingCdkCliError, Jt as AssetModeResolver, K as clearOnUpdateRemoval, Kn as LocalStartServiceError, Kt as escapeRegExp, L as yellow, Ln as resetAwsClients, Lt as rebuildClientForBucketRegion, M as bold, Mn as processStackMessages, Mt as withRetry, N as cyan, Nt as DagBuilder, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as PARTITION_TABLE, Ot as applyRoleArnIfSet, P as gray, Pn as resolveBucketRegion, Pt as TemplateParser, Q as disableInstanceApiTermination, Qn as ResourceTimeoutError, Qt as parseBootstrapMarker, R as collectDeclaredOutputNames, Rn as setAwsClients, Rt as shouldRetainResource, S as createPreDeleteFinalSnapshot, Sn as CFN_TEMPLATE_BODY_LIMIT, St as scrubResourceRecord, T as unsupportedFinalSnapshotError, Tn as findLargeInlineResources, Tt as s3BucketDualStackDomainName, U as stateKeySecretExposure, Un as DeployCancelledError, Ut as createAssetRedirectResolver, V as isExportAliasCollision, Vt as WorkGraph, W as IAMRoleProvider, Wn as LocalInvokeBuildError$1, Wt as loadPublishableAssetManifest, X as CloudControlProvider, Xn as PartialFailureError, Xt as ensureAssetStorage, Y as findSilentDropProperties, Yn as NestedStackChildDirectDestroyError, Yt as BOOTSTRAP_MARKER_PREFIX, Z as slowCcOperationTimeoutMs, Zn as ProvisioningError, Zt as getBootstrapMarkerKey, _ as computeImplicitDeleteEdges, _n as resolveStateBucketWithDefault, _t as STATE_SOURCED_READBACK_RULES, a as DeploymentEventsStore, an as runDockerForeground, at as normalizeAwsTagsToCfn, b as buildFinalSnapshotIdentifier, bn as stateBucketExistenceConfirmed, bt as maskSecretsInText, c as replayFailedOperations, cn as getDockerImageBySourceHash, cr as isMarkedNonRetryable, ct as coerceCfnBoolean, d as updatePartialReason, dn as getDefaultStateBucketName, dr as markNonRetryable, dt as readConfigString, en as validateContainerRepoName, er as StackHasActiveImportsError, et as IntrinsicFunctionResolver, f as UNSPECIFIED_SKIP_REASON, fn as getLegacyStateBucketName, ft as replayWarn, g as IMPLICIT_DELETE_DEPENDENCIES, gn as resolveSkipPrefix, gt as STATE_SOURCED_CROSS_GENERATION_RULES, h as maskingRetryLogger, hn as resolveCaptureObservedState, ht as requireConfigString, i as DeploymentEventsReader, in as getDockerCmd, it as WAFv2WebACLProvider, j as formatResourceLine, jn as AssemblyReader, jt as describeTypeWithThrottleRetry, k as isStatefulRecreateTargetSync, kn as canonicalizeRegion, kt as DiffCalculator, l as replayRollback, ln as Synthesizer, lr as isRetryableTransientError, lt as configBooleanRefusal, m as withResourceDeadline, mn as resolveAutoAssetStorage, mt as requireConfigObject, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as buildDockerImage, nt as getAccountInfo, o as planFailedOps, on as runDockerStreaming, or as normalizeAwsError, ot as resolveExplicitPhysicalId, p as deleteSkipReason, pn as resolveApp, pt as requireConfigArray, q as ProviderRegistry, qt as stripControlChars, r as DeployEngine, rn as formatDockerLoginError, rt as refStateLookupFromResource, s as planRollback, sn as AssetManifestLoader, sr as withErrorHandling, st as assertRegionMatch, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as buildDenyExternalAccessPolicy, tr as StackTerminationProtectionError, tt as cfnRefValueFromPhysicalId, u as updatePartialMessage, un as synthesisStatusMessage, ur as isThrottlingError, ut as configStringRefusal, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as resolveStateBucketWithDefaultAndSource, vt as TEMPLATE_SOURCED_RULES, w as refusesFinalSnapshot, wn as MIGRATE_TMP_PREFIX, wt as s3BucketDomainName, x as ccRoutedFinalSnapshotError, xn as warnDeprecatedNoPrefixCliFlag, xt as redactSecretsForState, y as PRE_DELETE_SNAPSHOT_TYPES, yn as resolveUseCdkBootstrapAssets, yt as createSecretMasker, z as collectPublishedOutputNames, zt as AssetPublisher } from "./deploy-engine-BNujuzW5.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-H-Dl8qng.js";
4
+ import { t as ASGProvider } from "./asg-provider-BZwQzb8e.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";
@@ -9808,6 +9808,8 @@ var SNSTopicProvider = class {
9808
9808
  * only.
9809
9809
  */
9810
9810
  async create(logicalId, resourceType, properties, context) {
9811
+ const maskSecrets = context?.maskSecrets ?? ((text) => text);
9812
+ const warn = (message) => this.logger.warn(maskSecrets(message));
9811
9813
  this.logger.debug(`Creating SNS topic ${logicalId}`);
9812
9814
  const topicName = properties["TopicName"] || generateResourceName(logicalId, { maxLength: 256 });
9813
9815
  try {
@@ -9845,7 +9847,7 @@ var SNSTopicProvider = class {
9845
9847
  AttributeValue: dataProtectionPolicy
9846
9848
  }));
9847
9849
  }
9848
- const deliveryStatusAttributes = buildDeliveryStatusAttributeMap(properties["DeliveryStatusLogging"], logicalId, context?.replayingState === true ? "warn" : "throw");
9850
+ const deliveryStatusAttributes = buildDeliveryStatusAttributeMap(properties["DeliveryStatusLogging"], logicalId, context?.replayingState === true ? "warn" : "throw", maskSecrets);
9849
9851
  for (const [attributeName, attributeValue] of deliveryStatusAttributes) await this.snsClient.send(new SetTopicAttributesCommand({
9850
9852
  TopicArn: topicArn,
9851
9853
  AttributeName: attributeName,
@@ -9860,7 +9862,7 @@ var SNSTopicProvider = class {
9860
9862
  await this.snsClient.send(new DeleteTopicCommand({ TopicArn: topicArn }));
9861
9863
  this.logger.debug(`Cleaned up partially-created SNS topic ${logicalId} (${topicArn}) after wiring failure`);
9862
9864
  } catch (cleanupError) {
9863
- this.logger.warn(`Failed to clean up partially-created SNS topic ${logicalId} (${topicArn}): ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}. Manual deletion may be required before the next deploy: aws sns delete-topic --topic-arn ${topicArn}`);
9865
+ warn(`Failed to clean up partially-created SNS topic ${logicalId} (${topicArn}): ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}. Manual deletion may be required before the next deploy: aws sns delete-topic --topic-arn ${topicArn}`);
9864
9866
  }
9865
9867
  throw innerError;
9866
9868
  }
@@ -9894,16 +9896,16 @@ var SNSTopicProvider = class {
9894
9896
  * matches those by class to decide its fallback, so swallowing one into a
9895
9897
  * ProvisioningError would silently disable that path.
9896
9898
  */
9897
- async update(logicalId, physicalId, resourceType, properties, previousProperties) {
9899
+ async update(logicalId, physicalId, resourceType, properties, previousProperties, context) {
9898
9900
  try {
9899
- return await this.applyUpdate(logicalId, physicalId, properties, previousProperties);
9901
+ return await this.applyUpdate(logicalId, physicalId, properties, previousProperties, context?.maskSecrets);
9900
9902
  } catch (error) {
9901
9903
  if (error instanceof CdkdError) throw error;
9902
9904
  const cause = error instanceof Error ? error : void 0;
9903
9905
  throw new ProvisioningError(`Failed to update SNS topic ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
9904
9906
  }
9905
9907
  }
9906
- async applyUpdate(logicalId, physicalId, properties, previousProperties) {
9908
+ async applyUpdate(logicalId, physicalId, properties, previousProperties, maskSecrets) {
9907
9909
  this.logger.debug(`Updating SNS topic ${logicalId}: ${physicalId}`);
9908
9910
  for (const attr of [
9909
9911
  {
@@ -9957,7 +9959,7 @@ var SNSTopicProvider = class {
9957
9959
  }
9958
9960
  }
9959
9961
  if (JSON.stringify(properties["DeliveryStatusLogging"]) !== JSON.stringify(previousProperties["DeliveryStatusLogging"])) {
9960
- const desiredAttributes = buildDeliveryStatusAttributeMap(properties["DeliveryStatusLogging"], logicalId, "warn");
9962
+ const desiredAttributes = buildDeliveryStatusAttributeMap(properties["DeliveryStatusLogging"], logicalId, "warn", maskSecrets);
9961
9963
  const previousAttributes = buildDeliveryStatusAttributeMap(previousProperties["DeliveryStatusLogging"], logicalId, "skip");
9962
9964
  for (const [attributeName, attributeValue] of desiredAttributes) {
9963
9965
  if (previousAttributes.get(attributeName) === attributeValue) continue;
@@ -10386,37 +10388,63 @@ const SNS_DELIVERY_STATUS_ATTRIBUTE_SUFFIXES = [
10386
10388
  * while `create()` kept `'throw'`; issue #1551 then extended the downgrade to
10387
10389
  * a create that DECLARES a state replay, leaving the template-path create
10388
10390
  * strict (#1529 / #1536 deliberately left that contract unchanged).
10389
- */
10390
- function buildDeliveryStatusAttributeMap(logging, logicalId, onMalformed) {
10391
+ *
10392
+ * `maskSecrets` is the caller's secret masker (issue #1932 item 3, adopted here
10393
+ * by issue #1997), threaded from `CreateContext` / `UpdateContext` — see
10394
+ * `SecretMaskingContext` in `src/types/resource.ts`. Every warning below names a
10395
+ * RESOLVED property value: `DeliveryStatusLogging` reaches this function already
10396
+ * resolved, so a `{{resolve:secretsmanager:...}}` scalar inside it is plaintext,
10397
+ * and cdkd's other masking boundaries (the deploy engine's error / reason text
10398
+ * and the resolver's debug line) do not cover a provider's own `logger.warn`.
10399
+ * It defaults to IDENTITY so every existing caller — the diff / import paths and
10400
+ * the unit tests — keeps working unchanged.
10401
+ *
10402
+ * The `'throw'` arm is masked too, and the reason is worth stating because the
10403
+ * first cut of this change got it backwards. It claimed a thrown message travels
10404
+ * to the deploy engine "which already runs it through `maskSecretsInText`" —
10405
+ * FALSE. The engine masks an error message on ONE path only, the events store
10406
+ * (`deploy-engine.ts`'s per-event masker); its CLI path logs `error.message`
10407
+ * RAW to `logger.error`. So an unmasked throw here printed the resolved bag
10408
+ * straight to the terminal at default verbosity, which is a worse exposure than
10409
+ * the warn arms this change was written for, not a covered one. Both throw sites
10410
+ * therefore run their interpolated value through the same `maskLeaf` walk.
10411
+ */
10412
+ function buildDeliveryStatusAttributeMap(logging, logicalId, onMalformed, maskSecrets = (text) => text) {
10391
10413
  const map = /* @__PURE__ */ new Map();
10392
10414
  if (logging == null) return map;
10393
10415
  const warn = (message) => {
10394
- getLogger().child("SNSTopicProvider").warn(message);
10416
+ getLogger().child("SNSTopicProvider").warn(maskSecrets(message));
10417
+ };
10418
+ const maskLeaf = (value) => {
10419
+ if (typeof value === "string") return maskSecrets(value);
10420
+ if (Array.isArray(value)) return value.map(maskLeaf);
10421
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [maskSecrets(k), maskLeaf(v)]));
10422
+ return value;
10395
10423
  };
10396
10424
  const seenProtocols = /* @__PURE__ */ new Set();
10397
10425
  if (!Array.isArray(logging)) {
10398
- if (onMalformed === "throw") throw new Error(`SNS topic ${logicalId}: DeliveryStatusLogging must be an array of {Protocol, ...} objects, got ${JSON.stringify(logging)}`);
10399
- if (onMalformed === "warn") warn(`SNS topic ${logicalId}: DeliveryStatusLogging must be an array of {Protocol, ...} objects, got ${JSON.stringify(logging)} — treating it as empty, so no delivery-status attribute is applied (on an update, any previously set one is reset). The desired properties can come from a cdkd state record (rollback replay / drift --revert), so this does not fail the operation; fix the template (or the state record on a replay) to clear this warning.`);
10426
+ if (onMalformed === "throw") throw new Error(`SNS topic ${logicalId}: DeliveryStatusLogging must be an array of {Protocol, ...} objects, got ${JSON.stringify(maskLeaf(logging))}`);
10427
+ if (onMalformed === "warn") warn(`SNS topic ${logicalId}: DeliveryStatusLogging must be an array of {Protocol, ...} objects, got ${JSON.stringify(maskLeaf(logging))} — treating it as empty, so no delivery-status attribute is applied (on an update, any previously set one is reset). The desired properties can come from a cdkd state record (rollback replay / drift --revert), so this does not fail the operation; fix the template (or the state record on a replay) to clear this warning.`);
10400
10428
  return map;
10401
10429
  }
10402
10430
  for (const entry of logging) {
10403
10431
  if (entry == null || typeof entry !== "object" || Array.isArray(entry)) {
10404
- if (onMalformed === "throw") throw new Error(`SNS topic ${logicalId}: DeliveryStatusLogging entries must be {Protocol, ...} objects, got ${JSON.stringify(entry)}`);
10405
- if (onMalformed === "warn") warn(`SNS topic ${logicalId}: skipping DeliveryStatusLogging entry ${JSON.stringify(entry)} — entries must be {Protocol, ...} objects. Attributes previously set for the skipped entry's protocol are reset, as if the entry had been removed.`);
10432
+ if (onMalformed === "throw") throw new Error(`SNS topic ${logicalId}: DeliveryStatusLogging entries must be {Protocol, ...} objects, got ${JSON.stringify(maskLeaf(entry))}`);
10433
+ if (onMalformed === "warn") warn(`SNS topic ${logicalId}: skipping DeliveryStatusLogging entry ${JSON.stringify(maskLeaf(entry))} — entries must be {Protocol, ...} objects. Attributes previously set for the skipped entry's protocol are reset, as if the entry had been removed.`);
10406
10434
  continue;
10407
10435
  }
10408
10436
  const config = entry;
10409
10437
  let protocol;
10410
- if (onMalformed === "throw") protocol = normalizeDeliveryStatusProtocolOrThrow(config["Protocol"], logicalId);
10438
+ if (onMalformed === "throw") protocol = normalizeDeliveryStatusProtocolOrThrow(config["Protocol"], logicalId, maskLeaf);
10411
10439
  else {
10412
10440
  const normalized = normalizeDeliveryStatusProtocol(config["Protocol"]);
10413
10441
  if (normalized === void 0) {
10414
- if (onMalformed === "warn") warn(`SNS topic ${logicalId}: skipping DeliveryStatusLogging entry with unsupported protocol ${JSON.stringify(config["Protocol"])}. Expected one of ${SNS_DELIVERY_STATUS_PROTOCOL_SPELLINGS.join(", ")} (case-insensitive). Attributes previously set for the skipped entry's protocol are reset, as if the entry had been removed.`);
10442
+ if (onMalformed === "warn") warn(`SNS topic ${logicalId}: skipping DeliveryStatusLogging entry with unsupported protocol ${JSON.stringify(maskLeaf(config["Protocol"]))}. Expected one of ${SNS_DELIVERY_STATUS_PROTOCOL_SPELLINGS.join(", ")} (case-insensitive). Attributes previously set for the skipped entry's protocol are reset, as if the entry had been removed.`);
10415
10443
  continue;
10416
10444
  }
10417
10445
  protocol = normalized;
10418
10446
  }
10419
- if (onMalformed !== "skip" && seenProtocols.has(protocol)) warn(`SNS topic ${logicalId}: DeliveryStatusLogging declares more than one entry for the "${protocol}" attribute prefix (${JSON.stringify(config["Protocol"])} canonicalizes to it). AWS has one attribute set per prefix, so these are merged per field — a later entry overwrites only the sub-fields it declares, and a field it omits keeps the earlier entry's value. Declare a single entry per protocol.`);
10447
+ if (onMalformed !== "skip" && seenProtocols.has(protocol)) warn(`SNS topic ${logicalId}: DeliveryStatusLogging declares more than one entry for the "${protocol}" attribute prefix (${JSON.stringify(maskLeaf(config["Protocol"]))} canonicalizes to it). AWS has one attribute set per prefix, so these are merged per field — a later entry overwrites only the sub-fields it declares, and a field it omits keeps the earlier entry's value. Declare a single entry per protocol.`);
10420
10448
  seenProtocols.add(protocol);
10421
10449
  for (const suffix of SNS_DELIVERY_STATUS_ATTRIBUTE_SUFFIXES) {
10422
10450
  const value = config[suffix];
@@ -10500,9 +10528,9 @@ function normalizeDeliveryStatusProtocol(input) {
10500
10528
  * #1538) because its bag can be a cdkd state record on a rollback replay
10501
10529
  * or `drift --revert`.
10502
10530
  */
10503
- function normalizeDeliveryStatusProtocolOrThrow(input, logicalId) {
10531
+ function normalizeDeliveryStatusProtocolOrThrow(input, logicalId, maskValue = (value) => value) {
10504
10532
  const normalized = normalizeDeliveryStatusProtocol(input);
10505
- if (normalized === void 0) throw new Error(`SNS topic ${logicalId}: unsupported DeliveryStatusLogging protocol ${JSON.stringify(input)}. Expected one of ${SNS_DELIVERY_STATUS_PROTOCOL_SPELLINGS.join(", ")} (case-insensitive).`);
10533
+ if (normalized === void 0) throw new Error(`SNS topic ${logicalId}: unsupported DeliveryStatusLogging protocol ${JSON.stringify(maskValue(input))}. Expected one of ${SNS_DELIVERY_STATUS_PROTOCOL_SPELLINGS.join(", ")} (case-insensitive).`);
10506
10534
  return normalized;
10507
10535
  }
10508
10536
  /**
@@ -15687,6 +15715,29 @@ function desiredIndexEntriesByName(value) {
15687
15715
  * it meant to exercise fails silently — it just gets faster.
15688
15716
  */
15689
15717
  const deleteTableRetryDelays$1 = {};
15718
+ /**
15719
+ * Mask every string LEAF (and every KEY) of a value before it is stringified
15720
+ * into a log line (issue #1932 item 3, adopted here by issue #1997).
15721
+ *
15722
+ * This is the INNER of the two masking layers `SecretMaskingContext` prescribes,
15723
+ * and neither layer subsumes the other. The outer one — routing the assembled
15724
+ * message through the masker — can only reach `maskSecretsInText`'s SUBSTRING
15725
+ * arm, which ignores needles below `MIN_NEEDLE_LENGTH` (4) and, more
15726
+ * importantly, never matches at all once `JSON.stringify` has escaped a `"`, a
15727
+ * `\` or a newline inside the value: the secret no longer OCCURS in the finished
15728
+ * line. Handing the masker each raw string reaches the WHOLE-VALUE arm at any
15729
+ * length and runs BEFORE any escaping. The mask is idempotent and matches by
15730
+ * value rather than by position, so applying both composes.
15731
+ *
15732
+ * Walks rather than testing the top level, because a secret nested in an object
15733
+ * leaf is stringified — and escaped — identically.
15734
+ */
15735
+ function maskLeafValue$1(value, maskSecrets) {
15736
+ if (typeof value === "string") return maskSecrets(value);
15737
+ if (Array.isArray(value)) return value.map((v) => maskLeafValue$1(v, maskSecrets));
15738
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [maskSecrets(k), maskLeafValue$1(v, maskSecrets)]));
15739
+ return value;
15740
+ }
15690
15741
  var DynamoDBTableProvider = class {
15691
15742
  dynamoDBClient;
15692
15743
  logger = getLogger().child("DynamoDBTableProvider");
@@ -15756,6 +15807,9 @@ var DynamoDBTableProvider = class {
15756
15807
  * Create a DynamoDB table
15757
15808
  */
15758
15809
  async create(logicalId, resourceType, properties, context) {
15810
+ const maskSecrets = context?.maskSecrets ?? ((text) => text);
15811
+ const warn = (message) => this.logger.warn(maskSecrets(message));
15812
+ const debug = (message) => this.logger.debug(maskSecrets(message));
15759
15813
  this.logger.debug(`Creating DynamoDB table ${logicalId}`);
15760
15814
  const tableName = properties["TableName"] || generateResourceName(logicalId, { maxLength: 255 });
15761
15815
  const keySchema = properties["KeySchema"];
@@ -15779,16 +15833,16 @@ var DynamoDBTableProvider = class {
15779
15833
  };
15780
15834
  }
15781
15835
  if (properties["OnDemandThroughput"]) createParams.OnDemandThroughput = properties["OnDemandThroughput"];
15782
- const createWarmThroughput = this.coerceWarmThroughputForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"]);
15836
+ const createWarmThroughput = this.coerceWarmThroughputForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"], maskSecrets);
15783
15837
  if (createWarmThroughput) createParams.WarmThroughput = createWarmThroughput;
15784
- else this.warnRefusedWarmThroughput(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"]);
15838
+ else this.warnRefusedWarmThroughput(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"], maskSecrets);
15785
15839
  if (properties["StreamSpecification"]) createParams.StreamSpecification = {
15786
15840
  StreamEnabled: true,
15787
15841
  StreamViewType: properties["StreamSpecification"]["StreamViewType"]
15788
15842
  };
15789
15843
  if (properties["GlobalSecondaryIndexes"]) {
15790
15844
  const declaredGsis = properties["GlobalSecondaryIndexes"];
15791
- createParams.GlobalSecondaryIndexes = Array.isArray(declaredGsis) ? declaredGsis.map((entry) => this.coerceIndexWarmThroughputForCreate(logicalId, entry)) : declaredGsis;
15845
+ createParams.GlobalSecondaryIndexes = Array.isArray(declaredGsis) ? declaredGsis.map((entry) => this.coerceIndexWarmThroughputForCreate(logicalId, entry, maskSecrets)) : declaredGsis;
15792
15846
  }
15793
15847
  if (properties["LocalSecondaryIndexes"]) createParams.LocalSecondaryIndexes = properties["LocalSecondaryIndexes"];
15794
15848
  const sse = mapSSESpecification(properties["SSESpecification"]);
@@ -15804,7 +15858,7 @@ var DynamoDBTableProvider = class {
15804
15858
  const tableInfo = await this.waitForTableActive(tableName);
15805
15859
  await this.applyPointInTimeRecovery(tableName, properties["PointInTimeRecoverySpecification"]);
15806
15860
  await this.applyTimeToLive(tableName, properties["TimeToLiveSpecification"]);
15807
- await this.applyKinesisStreamingDestination(tableName, properties["KinesisStreamSpecification"]);
15861
+ await this.applyKinesisStreamingDestination(tableName, properties["KinesisStreamSpecification"], void 0, maskSecrets);
15808
15862
  await this.applyContributorInsights(tableName, properties["ContributorInsightsSpecification"]);
15809
15863
  this.logger.debug(`Successfully created DynamoDB table ${logicalId}: ${tableName}`);
15810
15864
  return {
@@ -15819,9 +15873,9 @@ var DynamoDBTableProvider = class {
15819
15873
  } catch (error) {
15820
15874
  if (tableCreated) try {
15821
15875
  await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: tableName }));
15822
- this.logger.debug(`Rolled back partially-created DynamoDB table ${tableName}`);
15876
+ debug(`Rolled back partially-created DynamoDB table ${tableName}`);
15823
15877
  } catch (cleanupError) {
15824
- this.logger.warn(`Failed to roll back partially-created DynamoDB table ${tableName}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
15878
+ warn(`Failed to roll back partially-created DynamoDB table ${tableName}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
15825
15879
  }
15826
15880
  if (error instanceof ProvisioningError) throw error;
15827
15881
  const cause = error instanceof Error ? error : void 0;
@@ -15835,8 +15889,11 @@ var DynamoDBTableProvider = class {
15835
15889
  * For immutable property changes (KeySchema, etc.), the deployment layer
15836
15890
  * handles replacement via DELETE + CREATE.
15837
15891
  */
15838
- async update(logicalId, physicalId, resourceType, properties, previousProperties) {
15839
- this.logger.debug(`Updating DynamoDB table ${logicalId}: ${physicalId}`);
15892
+ async update(logicalId, physicalId, resourceType, properties, previousProperties, context) {
15893
+ const maskSecrets = context?.maskSecrets ?? ((text) => text);
15894
+ const warn = (message) => this.logger.warn(maskSecrets(message));
15895
+ const debug = (message) => this.logger.debug(maskSecrets(message));
15896
+ debug(`Updating DynamoDB table ${logicalId}: ${maskSecrets(physicalId)}`);
15840
15897
  try {
15841
15898
  const table = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }))).Table;
15842
15899
  let latestStreamArn = table?.LatestStreamArn;
@@ -15847,11 +15904,11 @@ var DynamoDBTableProvider = class {
15847
15904
  const recordedPrevBillingMode = previousProperties["BillingMode"];
15848
15905
  const recordedPrevBillingModeUsable = recordedPrevBillingMode === void 0 || typeof recordedPrevBillingMode === "string" && recordedPrevBillingMode.trim() !== "";
15849
15906
  const prevBillingMode = recordedPrevBillingModeUsable ? recordedPrevBillingMode ?? (properties["BillingMode"] !== void 0 ? liveBillingMode : "PROVISIONED") : liveBillingMode;
15850
- if (!recordedPrevBillingModeUsable) this.logger.warn(`AWS::DynamoDB::Table ${logicalId}: the recorded previous BillingMode is unusable (${JSON.stringify(recordedPrevBillingMode)}) — using the table's actual billing mode (${prevBillingMode}) as the comparison baseline for this update so a corrected template does not issue a same-mode UpdateTable.`);
15907
+ if (!recordedPrevBillingModeUsable) warn(`AWS::DynamoDB::Table ${logicalId}: the recorded previous BillingMode is unusable (${JSON.stringify(maskLeafValue$1(recordedPrevBillingMode, maskSecrets))}) — using the table's actual billing mode (${prevBillingMode}) as the comparison baseline for this update so a corrected template does not issue a same-mode UpdateTable.`);
15851
15908
  let billingModeUnusable = false;
15852
15909
  const requestedBillingMode = properties["BillingMode"] === void 0 ? "PROVISIONED" : requireConfigString(properties["BillingMode"], "PROVISIONED", "AWS::DynamoDB::Table BillingMode", { onUnusable: (message) => {
15853
15910
  billingModeUnusable = true;
15854
- this.logger.warn(`AWS::DynamoDB::Table ${logicalId}: ${message} The mode this update compared against (${prevBillingMode}) is kept rather than flipped to the default.`);
15911
+ warn(`AWS::DynamoDB::Table ${logicalId}: ${message} The mode this update compared against (${prevBillingMode}) is kept rather than flipped to the default.`);
15855
15912
  } });
15856
15913
  const billingMode = billingModeUnusable ? prevBillingMode : requestedBillingMode;
15857
15914
  const billingOrThroughputChanged = billingMode !== prevBillingMode || JSON.stringify(properties["ProvisionedThroughput"]) !== JSON.stringify(previousProperties["ProvisionedThroughput"]);
@@ -15883,7 +15940,7 @@ var DynamoDBTableProvider = class {
15883
15940
  const reasons = [];
15884
15941
  if (remainingWithoutCapacity.length > 0) reasons.push(`index(es) ${remainingWithoutCapacity.join(", ")} stay live and declare no usable per-index ProvisionedThroughput (both ReadCapacityUnits and WriteCapacityUnits are required there)`);
15885
15942
  if (tableCapacityUnusable) reasons.push(`the template declares no usable table-level ProvisionedThroughput`);
15886
- this.logger.warn(`AWS::DynamoDB::Table ${logicalId}: this deploy removes global secondary index(es) ${removable.join(", ")} and flips BillingMode to PROVISIONED. The removal would normally be applied BEFORE the flip, but ${reasons.join(" and ")}, so AWS rejects the flip either way. Nothing was removed.`);
15943
+ warn(`AWS::DynamoDB::Table ${logicalId}: this deploy removes global secondary index(es) ${removable.map((n) => maskSecrets(n)).join(", ")} and flips BillingMode to PROVISIONED. The removal would normally be applied BEFORE the flip, but ${reasons.join(" and ")}, so AWS rejects the flip either way. Nothing was removed.`);
15887
15944
  } else if (removable.length > 0) {
15888
15945
  this.logger.debug(`Deleting GSI(s) ${removable.join(", ")} on DynamoDB table ${physicalId} before the BillingMode flip to PROVISIONED`);
15889
15946
  await this.runGsiOps(physicalId, removable.map((name) => ({ Delete: { IndexName: name } })), void 0);
@@ -15929,7 +15986,7 @@ var DynamoDBTableProvider = class {
15929
15986
  gsiHandledByBillingFlip.add(indexName);
15930
15987
  }
15931
15988
  if (indexUpdates.length > 0) updateInput.GlobalSecondaryIndexUpdates = indexUpdates;
15932
- if (unspecified.length > 0) this.logger.warn(`AWS::DynamoDB::Table ${logicalId}: flipping BillingMode to PROVISIONED, but the template declares no usable ProvisionedThroughput for live index(es) ${unspecified.join(", ")}. AWS requires per-index capacity in the same UpdateTable and will reject the flip naming them. Declare GlobalSecondaryIndexes[].ProvisionedThroughput (both ReadCapacityUnits and WriteCapacityUnits) for each. An index this deploy REMOVES is deleted BEFORE the flip automatically (issue #1617), so a name here is one the template still keeps, one that exists in AWS but in neither the template nor cdkd state (created out of band), or one whose pre-flip removal was skipped for the reason warned above.`);
15989
+ if (unspecified.length > 0) warn(`AWS::DynamoDB::Table ${logicalId}: flipping BillingMode to PROVISIONED, but the template declares no usable ProvisionedThroughput for live index(es) ${unspecified.map((n) => maskSecrets(n)).join(", ")}. AWS requires per-index capacity in the same UpdateTable and will reject the flip naming them. Declare GlobalSecondaryIndexes[].ProvisionedThroughput (both ReadCapacityUnits and WriteCapacityUnits) for each. An index this deploy REMOVES is deleted BEFORE the flip automatically (issue #1617), so a name here is one the template still keeps, one that exists in AWS but in neither the template nor cdkd state (created out of band), or one whose pre-flip removal was skipped for the reason warned above.`);
15933
15990
  }
15934
15991
  await this.dynamoDBClient.send(new UpdateTableCommand(updateInput));
15935
15992
  if (billingMode !== liveBillingMode) await this.waitForTableAndIndexesActive(physicalId, BILLING_FLIP_ACTIVE_WAIT_ATTEMPTS);
@@ -15947,9 +16004,9 @@ var DynamoDBTableProvider = class {
15947
16004
  }
15948
16005
  }
15949
16006
  if (JSON.stringify(properties["WarmThroughput"]) !== JSON.stringify(previousProperties["WarmThroughput"])) {
15950
- this.warnRefusedWarmThroughput(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"]);
15951
- const warmThroughputToSend = this.coerceWarmThroughputForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"]);
15952
- if (warmThroughputToSend !== void 0 && !this.skipWarmThroughputDecrease(logicalId, physicalId, warmThroughputToSend, table?.WarmThroughput)) {
16007
+ this.warnRefusedWarmThroughput(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"], maskSecrets);
16008
+ const warmThroughputToSend = this.coerceWarmThroughputForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"], maskSecrets);
16009
+ if (warmThroughputToSend !== void 0 && !this.skipWarmThroughputDecrease(logicalId, physicalId, warmThroughputToSend, table?.WarmThroughput, maskSecrets)) {
15953
16010
  await this.dynamoDBClient.send(new UpdateTableCommand({
15954
16011
  TableName: physicalId,
15955
16012
  WarmThroughput: warmThroughputToSend
@@ -16009,7 +16066,7 @@ var DynamoDBTableProvider = class {
16009
16066
  this.logger.debug(`Disabled DynamoDB Stream on table ${physicalId}`);
16010
16067
  }
16011
16068
  }
16012
- if (JSON.stringify(properties["GlobalSecondaryIndexes"]) !== JSON.stringify(previousProperties["GlobalSecondaryIndexes"])) await this.applyGsiUpdates(physicalId, resourceType, logicalId, previousProperties["GlobalSecondaryIndexes"], properties["GlobalSecondaryIndexes"], properties["AttributeDefinitions"], gsiHandledByBillingFlip, currentLiveIndexNames, currentLiveIndexByName, liveCapacityComparable);
16069
+ if (JSON.stringify(properties["GlobalSecondaryIndexes"]) !== JSON.stringify(previousProperties["GlobalSecondaryIndexes"])) await this.applyGsiUpdates(physicalId, resourceType, logicalId, previousProperties["GlobalSecondaryIndexes"], properties["GlobalSecondaryIndexes"], properties["AttributeDefinitions"], gsiHandledByBillingFlip, currentLiveIndexNames, currentLiveIndexByName, liveCapacityComparable, maskSecrets);
16013
16070
  if (JSON.stringify(properties["PointInTimeRecoverySpecification"]) !== JSON.stringify(previousProperties["PointInTimeRecoverySpecification"])) await this.applyPointInTimeRecovery(physicalId, properties["PointInTimeRecoverySpecification"], previousProperties["PointInTimeRecoverySpecification"]);
16014
16071
  if (JSON.stringify(properties["TimeToLiveSpecification"]) !== JSON.stringify(previousProperties["TimeToLiveSpecification"])) {
16015
16072
  this.assertNoActiveTtlAttributeNameChange(logicalId, resourceType, physicalId, properties["TimeToLiveSpecification"], previousProperties["TimeToLiveSpecification"]);
@@ -16019,7 +16076,7 @@ var DynamoDBTableProvider = class {
16019
16076
  if (!table?.TableArn) throw new ProvisioningError(`Cannot apply ResourcePolicy change for DynamoDB table ${logicalId}: DescribeTable returned no TableArn`, resourceType, logicalId, physicalId);
16020
16077
  await this.applyResourcePolicy(table.TableArn, properties["ResourcePolicy"], previousProperties["ResourcePolicy"]);
16021
16078
  }
16022
- if (JSON.stringify(properties["KinesisStreamSpecification"]) !== JSON.stringify(previousProperties["KinesisStreamSpecification"])) await this.applyKinesisStreamingDestination(physicalId, properties["KinesisStreamSpecification"], previousProperties["KinesisStreamSpecification"]);
16079
+ if (JSON.stringify(properties["KinesisStreamSpecification"]) !== JSON.stringify(previousProperties["KinesisStreamSpecification"])) await this.applyKinesisStreamingDestination(physicalId, properties["KinesisStreamSpecification"], previousProperties["KinesisStreamSpecification"], maskSecrets);
16023
16080
  if (JSON.stringify(properties["ContributorInsightsSpecification"]) !== JSON.stringify(previousProperties["ContributorInsightsSpecification"])) await this.applyContributorInsights(physicalId, properties["ContributorInsightsSpecification"], previousProperties["ContributorInsightsSpecification"]);
16024
16081
  return {
16025
16082
  physicalId,
@@ -16317,11 +16374,11 @@ var DynamoDBTableProvider = class {
16317
16374
  * pre-existing WarmThroughput "no clean remap" stance; precision changes flow
16318
16375
  * through on the create / first-enable path.
16319
16376
  */
16320
- async applyKinesisStreamingDestination(tableName, spec, previousSpec) {
16377
+ async applyKinesisStreamingDestination(tableName, spec, previousSpec, maskSecrets = (text) => text) {
16321
16378
  const newArn = this.extractKinesisStreamArn(spec);
16322
16379
  const prevArn = this.extractKinesisStreamArn(previousSpec);
16323
16380
  if (newArn === prevArn) {
16324
- if (newArn && JSON.stringify(spec?.["ApproximateCreationDateTimePrecision"]) !== JSON.stringify(previousSpec?.["ApproximateCreationDateTimePrecision"])) this.logger.warn(`Kinesis streaming ApproximateCreationDateTimePrecision change on ${tableName} was not applied (same stream ARN; precision-only updates are not yet supported)`);
16381
+ if (newArn && JSON.stringify(spec?.["ApproximateCreationDateTimePrecision"]) !== JSON.stringify(previousSpec?.["ApproximateCreationDateTimePrecision"])) this.logger.warn(maskSecrets(`Kinesis streaming ApproximateCreationDateTimePrecision change on ${tableName} was not applied (same stream ARN; precision-only updates are not yet supported)`));
16325
16382
  return;
16326
16383
  }
16327
16384
  if (prevArn) {
@@ -16507,7 +16564,8 @@ var DynamoDBTableProvider = class {
16507
16564
  * a remove-then-add pair, so this method throws on such a change rather than
16508
16565
  * silently dropping it.
16509
16566
  */
16510
- async applyGsiUpdates(physicalId, resourceType, logicalId, previousGsis, desiredGsis, desiredAttributeDefinitions, handledByBillingFlip = /* @__PURE__ */ new Set(), liveIndexNames, liveIndexByName, liveCapacityComparable = false) {
16567
+ async applyGsiUpdates(physicalId, resourceType, logicalId, previousGsis, desiredGsis, desiredAttributeDefinitions, handledByBillingFlip = /* @__PURE__ */ new Set(), liveIndexNames, liveIndexByName, liveCapacityComparable = false, maskSecrets = (text) => text) {
16568
+ const warn = (message) => this.logger.warn(maskSecrets(message));
16511
16569
  const prev = previousGsis ?? [];
16512
16570
  const desired = desiredGsis ?? [];
16513
16571
  const prevByName = new Map(prev.filter((g) => g.IndexName).map((g) => [g.IndexName, g]));
@@ -16525,24 +16583,24 @@ var DynamoDBTableProvider = class {
16525
16583
  const live = liveIndexByName?.get(name);
16526
16584
  if (!gsi.KeySchema) throw new ProvisioningError(`GlobalSecondaryIndex ${name} on DynamoDB table ${logicalId} is missing KeySchema`, resourceType, logicalId, physicalId);
16527
16585
  if (live !== void 0 && live.IndexStatus !== "DELETING") {
16528
- this.logger.warn(`GSI ${name} on DynamoDB table ${physicalId} already exists in AWS but is absent from cdkd's recorded previous state, so its Create is being skipped. This usually means an earlier deploy created the index and then failed before state was written.`);
16586
+ warn(`GSI ${maskSecrets(name)} on DynamoDB table ${physicalId} already exists in AWS but is absent from cdkd's recorded previous state, so its Create is being skipped. This usually means an earlier deploy created the index and then failed before state was written.`);
16529
16587
  const adopted = { IndexName: name };
16530
16588
  let adoptedHasMember = false;
16531
- if (liveCapacityComparable && gsi.ProvisionedThroughput && !handledByBillingFlip.has(name) && !liveCapacityAlreadyMatches(live.ProvisionedThroughput, gsi.ProvisionedThroughput) && !this.skipZeroCapacityIndexUpdate(name, physicalId, gsi.ProvisionedThroughput)) {
16589
+ if (liveCapacityComparable && gsi.ProvisionedThroughput && !handledByBillingFlip.has(name) && !liveCapacityAlreadyMatches(live.ProvisionedThroughput, gsi.ProvisionedThroughput) && !this.skipZeroCapacityIndexUpdate(name, physicalId, gsi.ProvisionedThroughput, maskSecrets)) {
16532
16590
  adopted.ProvisionedThroughput = gsi.ProvisionedThroughput;
16533
16591
  adoptedHasMember = true;
16534
16592
  }
16535
- const adoptedWarm = this.warmThroughputOpFor(name, physicalId, gsi, void 0, live);
16593
+ const adoptedWarm = this.warmThroughputOpFor(name, physicalId, gsi, void 0, live, maskSecrets);
16536
16594
  if (adoptedWarm) {
16537
16595
  adopted.WarmThroughput = adoptedWarm;
16538
16596
  adoptedHasMember = true;
16539
16597
  }
16540
16598
  if (adoptedHasMember) ops.push({ Update: adopted });
16541
- this.logger.warn(`GSI ${name} was adopted from AWS rather than created, so cdkd could not verify its KeySchema / Projection match the template. Run \`cdkd drift ${physicalId}\` to confirm, and \`cdkd drift --accept\` if AWS is the source of truth.`);
16599
+ warn(`GSI ${maskSecrets(name)} was adopted from AWS rather than created, so cdkd could not verify its KeySchema / Projection match the template. Run \`cdkd drift ${physicalId}\` to confirm, and \`cdkd drift --accept\` if AWS is the source of truth.`);
16542
16600
  continue;
16543
16601
  }
16544
- this.warnRefusedWarmThroughput(`GSI ${name} on DynamoDB table ${physicalId}`, gsi.WarmThroughput);
16545
- const createWarm = this.coerceWarmThroughputForSend(`GSI ${name} on DynamoDB table ${physicalId}`, gsi.WarmThroughput);
16602
+ this.warnRefusedWarmThroughput(`GSI ${maskSecrets(name)} on DynamoDB table ${physicalId}`, gsi.WarmThroughput, maskSecrets);
16603
+ const createWarm = this.coerceWarmThroughputForSend(`GSI ${maskSecrets(name)} on DynamoDB table ${physicalId}`, gsi.WarmThroughput, maskSecrets);
16546
16604
  ops.push({ Create: {
16547
16605
  IndexName: name,
16548
16606
  KeySchema: gsi.KeySchema,
@@ -16558,12 +16616,12 @@ var DynamoDBTableProvider = class {
16558
16616
  let updateHasMember = false;
16559
16617
  if (gsi.ProvisionedThroughput && !handledByBillingFlip.has(name) && JSON.stringify(before.ProvisionedThroughput) !== JSON.stringify(gsi.ProvisionedThroughput)) {
16560
16618
  if (liveCapacityComparable && liveCapacityAlreadyMatches(liveIndexByName?.get(name)?.ProvisionedThroughput, gsi.ProvisionedThroughput)) this.logger.debug(`GSI ${name} on DynamoDB table ${physicalId} already carries the requested capacity in AWS; skipping its throughput Update`);
16561
- else if (!this.skipZeroCapacityIndexUpdate(name, physicalId, gsi.ProvisionedThroughput)) {
16619
+ else if (!this.skipZeroCapacityIndexUpdate(name, physicalId, gsi.ProvisionedThroughput, maskSecrets)) {
16562
16620
  update.ProvisionedThroughput = gsi.ProvisionedThroughput;
16563
16621
  updateHasMember = true;
16564
16622
  }
16565
16623
  }
16566
- const warm = this.warmThroughputOpFor(name, physicalId, gsi, before, liveIndexByName?.get(name));
16624
+ const warm = this.warmThroughputOpFor(name, physicalId, gsi, before, liveIndexByName?.get(name), maskSecrets);
16567
16625
  if (warm) {
16568
16626
  update.WarmThroughput = warm;
16569
16627
  updateHasMember = true;
@@ -16583,10 +16641,11 @@ var DynamoDBTableProvider = class {
16583
16641
  * two messages are disjoint by construction: this one fires only when
16584
16642
  * something DID resolve.
16585
16643
  */
16586
- coerceWarmThroughputForSend(scope, value) {
16644
+ coerceWarmThroughputForSend(scope, value, maskSecrets = (text) => text) {
16645
+ const warn = (message) => this.logger.warn(maskSecrets(message));
16587
16646
  const coerced = coerceWarmThroughput(value);
16588
16647
  if (coerced === void 0) return void 0;
16589
- if (coerced.dropped.length > 0) this.logger.warn(`${scope}: WarmThroughput member(s) ${coerced.dropped.join(", ")} in ${JSON.stringify(value)} are not a number DynamoDB accepts, so they were dropped from the request, which leaves ${JSON.stringify(coerced.spec)}. Check for an unresolved intrinsic or a non-numeric value.`);
16648
+ if (coerced.dropped.length > 0) warn(`${scope}: WarmThroughput member(s) ${coerced.dropped.join(", ")} in ${JSON.stringify(maskLeafValue$1(value, maskSecrets))} are not a number DynamoDB accepts, so they were dropped from the request, which leaves ${JSON.stringify(coerced.spec)}. Check for an unresolved intrinsic or a non-numeric value.`);
16590
16649
  return coerced.spec;
16591
16650
  }
16592
16651
  /**
@@ -16604,14 +16663,14 @@ var DynamoDBTableProvider = class {
16604
16663
  * is returned UNCHANGED — including the identity of the object, so the
16605
16664
  * common case allocates nothing.
16606
16665
  */
16607
- coerceIndexWarmThroughputForCreate(logicalId, entry) {
16666
+ coerceIndexWarmThroughputForCreate(logicalId, entry, maskSecrets = (text) => text) {
16608
16667
  if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
16609
16668
  const declared = entry["WarmThroughput"];
16610
16669
  if (declared === void 0) return entry;
16611
- const scope = `GSI ${entry.IndexName ?? "<unnamed>"} on AWS::DynamoDB::Table ${logicalId}`;
16612
- const coerced = this.coerceWarmThroughputForSend(scope, declared);
16670
+ const scope = `GSI ${entry.IndexName === void 0 ? "<unnamed>" : maskSecrets(entry.IndexName)} on AWS::DynamoDB::Table ${logicalId}`;
16671
+ const coerced = this.coerceWarmThroughputForSend(scope, declared, maskSecrets);
16613
16672
  if (coerced === void 0) {
16614
- this.warnRefusedWarmThroughput(scope, declared);
16673
+ this.warnRefusedWarmThroughput(scope, declared, maskSecrets);
16615
16674
  const { WarmThroughput: _dropped, ...rest } = entry;
16616
16675
  return rest;
16617
16676
  }
@@ -16632,9 +16691,10 @@ var DynamoDBTableProvider = class {
16632
16691
  * Silent for an ABSENT or FALSY value — see {@link isRefusedWarmThroughput}
16633
16692
  * for why those two are not this warning's business.
16634
16693
  */
16635
- warnRefusedWarmThroughput(scope, value) {
16694
+ warnRefusedWarmThroughput(scope, value, maskSecrets = (text) => text) {
16695
+ const warn = (message) => this.logger.warn(maskSecrets(message));
16636
16696
  if (!isRefusedWarmThroughput(value)) return;
16637
- this.logger.warn(`${scope}: the declared WarmThroughput ${JSON.stringify(value)} carries no usable ReadUnitsPerSecond or WriteUnitsPerSecond, so it was NOT sent — DynamoDB accepts only those two members and rejects a request without either. Check for a misspelled member name or an unresolved intrinsic. Nothing else on this resource was affected.`);
16697
+ warn(`${scope}: the declared WarmThroughput ${JSON.stringify(maskLeafValue$1(value, maskSecrets))} carries no usable ReadUnitsPerSecond or WriteUnitsPerSecond, so it was NOT sent — DynamoDB accepts only those two members and rejects a request without either. Check for a misspelled member name or an unresolved intrinsic. Nothing else on this resource was affected.`);
16638
16698
  }
16639
16699
  /**
16640
16700
  * Should this per-index capacity `Update` be SKIPPED because the value is
@@ -16689,10 +16749,11 @@ var DynamoDBTableProvider = class {
16689
16749
  * `DescribeTable` reports for every index anyway, so there is no divergence
16690
16750
  * to hide.
16691
16751
  */
16692
- skipZeroCapacityIndexUpdate(indexName, physicalId, requested) {
16752
+ skipZeroCapacityIndexUpdate(indexName, physicalId, requested, maskSecrets = (text) => text) {
16693
16753
  if (requested === void 0) return false;
16694
16754
  if (toFiniteNumber(requested.ReadCapacityUnits) !== 0 || toFiniteNumber(requested.WriteCapacityUnits) !== 0) return false;
16695
- this.logger.warn(`GSI ${indexName} on DynamoDB table ${physicalId}: the requested ProvisionedThroughput is {ReadCapacityUnits: 0, WriteCapacityUnits: 0}, which is AWS's on-demand placeholder rather than a capacity DynamoDB accepts (the minimum is 1), so no per-index throughput update was sent. This usually means the value came from a pre-#1767 cdkd state record — a \`cdkd drift --revert\` of such a record, or a rollback replaying it. Re-run \`cdkd deploy\` (or \`cdkd drift --accept\`) to refresh the record.`);
16755
+ const warn = (message) => this.logger.warn(maskSecrets(message));
16756
+ warn(`GSI ${maskSecrets(indexName)} on DynamoDB table ${physicalId}: the requested ProvisionedThroughput is {ReadCapacityUnits: 0, WriteCapacityUnits: 0}, which is AWS's on-demand placeholder rather than a capacity DynamoDB accepts (the minimum is 1), so no per-index throughput update was sent. This usually means the value came from a pre-#1767 cdkd state record — a \`cdkd drift --revert\` of such a record, or a rollback replaying it. Re-run \`cdkd deploy\` (or \`cdkd drift --accept\`) to refresh the record.`);
16696
16757
  return true;
16697
16758
  }
16698
16759
  /**
@@ -16721,20 +16782,23 @@ var DynamoDBTableProvider = class {
16721
16782
  * lower than current WarmThroughput, decreasing WarmThroughput is not
16722
16783
  * supported`.
16723
16784
  */
16724
- warmThroughputOpFor(indexName, physicalId, desired, previous, live) {
16785
+ warmThroughputOpFor(indexName, physicalId, desired, previous, live, maskSecrets = (text) => text) {
16786
+ const warn = (message) => this.logger.warn(maskSecrets(message));
16787
+ const debug = (message) => this.logger.debug(maskSecrets(message));
16788
+ const safeIndexName = maskSecrets(indexName);
16725
16789
  const requested = desired.WarmThroughput;
16726
16790
  if (previous !== void 0 && JSON.stringify(previous.WarmThroughput) === JSON.stringify(requested)) return;
16727
- const sendable = this.coerceWarmThroughputForSend(`GSI ${indexName} on DynamoDB table ${physicalId}`, requested);
16791
+ const sendable = this.coerceWarmThroughputForSend(`GSI ${safeIndexName} on DynamoDB table ${physicalId}`, requested, maskSecrets);
16728
16792
  if (sendable === void 0) {
16729
- this.warnRefusedWarmThroughput(`GSI ${indexName} on DynamoDB table ${physicalId}`, requested);
16793
+ this.warnRefusedWarmThroughput(`GSI ${safeIndexName} on DynamoDB table ${physicalId}`, requested, maskSecrets);
16730
16794
  return;
16731
16795
  }
16732
16796
  if (warmThroughputAlreadyMatches(sendable, live?.WarmThroughput)) {
16733
- this.logger.debug(`GSI ${indexName} on DynamoDB table ${physicalId} already carries the requested warm throughput in AWS; skipping its WarmThroughput Update`);
16797
+ debug(`GSI ${safeIndexName} on DynamoDB table ${physicalId} already carries the requested warm throughput in AWS; skipping its WarmThroughput Update`);
16734
16798
  return;
16735
16799
  }
16736
16800
  if (isWarmThroughputDecrease(sendable, live?.WarmThroughput)) {
16737
- this.logger.warn(`GSI ${indexName} on DynamoDB table ${physicalId}: the requested WarmThroughput ${JSON.stringify(sendable)} is lower than the ${JSON.stringify({
16801
+ warn(`GSI ${safeIndexName} on DynamoDB table ${physicalId}: the requested WarmThroughput ${JSON.stringify(sendable)} is lower than the ${JSON.stringify({
16738
16802
  ReadUnitsPerSecond: live?.WarmThroughput?.ReadUnitsPerSecond,
16739
16803
  WriteUnitsPerSecond: live?.WarmThroughput?.WriteUnitsPerSecond
16740
16804
  })} DynamoDB currently holds for that index. Warm throughput only ever RISES with an index's traffic and cannot be lowered — AWS rejects the UpdateTable with "decreasing WarmThroughput is not supported" — so it was NOT sent and every other change on this table still applied. On a deploy this warning may be the only signal you get: a successful deploy re-captures AWS's current value as the drift baseline. If your template declares this index's WarmThroughput, set it to the value AWS holds (or remove it, which AWS treats as keeping the last-set value); if it does not, the value came from a cdkd state record and 'cdkd deploy' (or 'cdkd drift --accept') refreshes it.`);
@@ -16810,9 +16874,10 @@ var DynamoDBTableProvider = class {
16810
16874
  * resource clean. The message therefore says the warning may be the only
16811
16875
  * signal, and names both numbers so the template edit needs no second look.
16812
16876
  */
16813
- skipWarmThroughputDecrease(logicalId, physicalId, desired, live) {
16877
+ skipWarmThroughputDecrease(logicalId, physicalId, desired, live, maskSecrets = (text) => text) {
16878
+ const warn = (message) => this.logger.warn(maskSecrets(message));
16814
16879
  if (!isWarmThroughputDecrease(desired, live)) return false;
16815
- this.logger.warn(`AWS::DynamoDB::Table ${logicalId}: the requested WarmThroughput ${JSON.stringify(desired)} is lower than the ${JSON.stringify({
16880
+ warn(`AWS::DynamoDB::Table ${logicalId}: the requested WarmThroughput ${JSON.stringify(desired)} is lower than the ${JSON.stringify({
16816
16881
  ReadUnitsPerSecond: live?.ReadUnitsPerSecond,
16817
16882
  WriteUnitsPerSecond: live?.WriteUnitsPerSecond
16818
16883
  })} DynamoDB currently holds for table ${physicalId}. Warm throughput only ever RISES with a table's traffic and cannot be lowered — AWS rejects the UpdateTable with "decreasing WarmThroughput is not supported" — so it was NOT sent and every other change on this resource still applied. On a deploy this warning may be the only signal you get: a successful deploy re-captures AWS's current value as the drift baseline, so the next 'cdkd drift' reports this table clean (a 'cdkd drift --revert' records nothing and keeps reporting it). To make cdkd and AWS agree, set WarmThroughput to the value AWS holds (or remove the property, which AWS treats as keeping the last-set value).`);
@@ -17215,6 +17280,34 @@ const DRIFT_STRIPPED_INDEX_MEMBERS = [{
17215
17280
  * client. The shared helper `applyCrossRegionReplicaTagsDiff`
17216
17281
  * centralizes the diff + best-effort WARN-on-failure contract.
17217
17282
  */
17283
+ /**
17284
+ * Mask every string LEAF (and every KEY) of a value before it is stringified
17285
+ * into a log line (issue #1932 item 3, adopted here by issue #1997).
17286
+ *
17287
+ * The INNER of the two masking layers `SecretMaskingContext` prescribes, and
17288
+ * neither layer subsumes the other. The outer one — routing the assembled
17289
+ * message through the masker — can only reach `maskSecretsInText`'s SUBSTRING
17290
+ * arm, which ignores needles below `MIN_NEEDLE_LENGTH` (4) and, more
17291
+ * importantly, never matches at all once `JSON.stringify` has escaped a `"`, a
17292
+ * `\` or a newline inside the value: the secret no longer OCCURS in the
17293
+ * finished line. Handing the masker each raw string reaches the WHOLE-VALUE arm
17294
+ * at any length and runs BEFORE any escaping. The mask is idempotent and matches
17295
+ * by value rather than by position, so the two layers compose.
17296
+ *
17297
+ * Walks rather than testing the top level, because a secret nested in an object
17298
+ * or array leaf is stringified — and escaped — identically. The sibling
17299
+ * `dynamodb-table-provider.ts` carries the same helper; it is duplicated rather
17300
+ * than shared because these two files deliberately hold no common module today
17301
+ * beyond the narrow rule modules (`dynamodb-warm-throughput.ts`,
17302
+ * `dynamodb-index-busy-delete.ts`), each of which exists for a MEASURED
17303
+ * divergence rather than for convenience.
17304
+ */
17305
+ function maskLeafValue(value, maskSecrets) {
17306
+ if (typeof value === "string") return maskSecrets(value);
17307
+ if (Array.isArray(value)) return value.map((v) => maskLeafValue(v, maskSecrets));
17308
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [maskSecrets(k), maskLeafValue(v, maskSecrets)]));
17309
+ return value;
17310
+ }
17218
17311
  var DynamoDBGlobalTableProvider = class {
17219
17312
  dynamoDBClient;
17220
17313
  logger = getLogger().child("DynamoDBGlobalTableProvider");
@@ -17400,6 +17493,8 @@ var DynamoDBGlobalTableProvider = class {
17400
17493
  * command.
17401
17494
  */
17402
17495
  async create(logicalId, resourceType, properties, context) {
17496
+ const maskSecrets = context?.maskSecrets ?? ((text) => text);
17497
+ const warn = (message) => this.logger.warn(maskSecrets(message));
17403
17498
  this.logger.debug(`Creating DynamoDB GlobalTable ${logicalId}`);
17404
17499
  const tableName = properties["TableName"] || generateResourceName(logicalId, { maxLength: 255 });
17405
17500
  const keySchema = properties["KeySchema"];
@@ -17417,7 +17512,7 @@ var DynamoDBGlobalTableProvider = class {
17417
17512
  } } : {});
17418
17513
  if (billingReplayWarn && properties["BillingMode"] === void 0) {
17419
17514
  billingModeAbsentOnReplay = true;
17420
- this.logger.warn(`AWS::DynamoDB::GlobalTable ${logicalId}: the state record declares no BillingMode, so this replay creates the table PAY_PER_REQUEST. If it was PROVISIONED, set the mode in the template and redeploy.`);
17515
+ warn(`AWS::DynamoDB::GlobalTable ${logicalId}: the state record declares no BillingMode, so this replay creates the table PAY_PER_REQUEST. If it was PROVISIONED, set the mode in the template and redeploy.`);
17421
17516
  }
17422
17517
  } catch (error) {
17423
17518
  throw new ProvisioningError(error instanceof Error ? error.message : String(error), resourceType, logicalId, void 0, error instanceof Error ? error : void 0);
@@ -17527,7 +17622,7 @@ var DynamoDBGlobalTableProvider = class {
17527
17622
  toSdkReplicaGlobalSecondaryIndexes(replica["GlobalSecondaryIndexes"], billingMode, diagnostics);
17528
17623
  toSdkReplicaThroughputOverrides(replica, billingMode, diagnostics, {});
17529
17624
  }
17530
- this.reportThroughputDiagnostics(diagnostics, logicalId, tableName);
17625
+ this.reportThroughputDiagnostics(diagnostics, logicalId, tableName, maskSecrets);
17531
17626
  try {
17532
17627
  await this.dynamoDBClient.send(new CreateTableCommand(createParams));
17533
17628
  this.logger.debug(`CreateTable initiated for ${tableName}, waiting for ACTIVE`);
@@ -17547,7 +17642,7 @@ var DynamoDBGlobalTableProvider = class {
17547
17642
  if (!region || region === currentRegion) continue;
17548
17643
  const replicaTags = replica["Tags"];
17549
17644
  if (!replicaTags || replicaTags.length === 0) continue;
17550
- await this.applyCrossRegionReplicaTagsDiff(tableInfo.tableArn, region, void 0, replicaTags, tableName);
17645
+ await this.applyCrossRegionReplicaTagsDiff(tableInfo.tableArn, region, void 0, replicaTags, tableName, maskSecrets);
17551
17646
  }
17552
17647
  if (properties["TimeToLiveSpecification"]) {
17553
17648
  const ttl = properties["TimeToLiveSpecification"];
@@ -17562,9 +17657,9 @@ var DynamoDBGlobalTableProvider = class {
17562
17657
  }));
17563
17658
  }
17564
17659
  if (billingMode === "PROVISIONED") try {
17565
- await this.reconcileAutoScalingTargets(tableName, [], collectAutoScalingTargets(properties, currentRegion), currentRegion);
17660
+ await this.reconcileAutoScalingTargets(tableName, [], collectAutoScalingTargets(properties, currentRegion), currentRegion, void 0, maskSecrets);
17566
17661
  } catch (autoScalingErr) {
17567
- this.logger.warn(`Auto-scaling registration failed for ${tableName}: ${autoScalingErr instanceof Error ? autoScalingErr.message : String(autoScalingErr)}. The table was created successfully; re-run the deploy to register the scaling targets.`);
17662
+ warn(`Auto-scaling registration failed for ${tableName}: ${autoScalingErr instanceof Error ? autoScalingErr.message : String(autoScalingErr)}. The table was created successfully; re-run the deploy to register the scaling targets.`);
17568
17663
  }
17569
17664
  this.logger.debug(`Successfully created DynamoDB GlobalTable ${logicalId}: ${tableName}`);
17570
17665
  return {
@@ -17578,7 +17673,7 @@ var DynamoDBGlobalTableProvider = class {
17578
17673
  ...effectiveProperties && { effectiveProperties }
17579
17674
  };
17580
17675
  } catch (wiringError) {
17581
- this.logger.warn(`Wiring failed after CreateTable for ${tableName}; attempting best-effort cleanup`);
17676
+ warn(`Wiring failed after CreateTable for ${tableName}; attempting best-effort cleanup`);
17582
17677
  try {
17583
17678
  const replicasForCleanup = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.Replicas ?? [];
17584
17679
  for (const replica of replicasForCleanup) {
@@ -17591,14 +17686,12 @@ var DynamoDBGlobalTableProvider = class {
17591
17686
  }));
17592
17687
  await this.waitForReplicaGone(tableName, region, logicalId);
17593
17688
  } catch (replicaCleanupErr) {
17594
- const msg = replicaCleanupErr instanceof Error ? replicaCleanupErr.message : String(replicaCleanupErr);
17595
- this.logger.warn(`Partial-create cleanup: failed to drop replica ${region} on ${tableName}: ${msg}. Run: aws dynamodb update-table --table-name ${tableName} --replica-updates 'Delete={RegionName=${region}}' --region ${currentRegion}`);
17689
+ warn(`Partial-create cleanup: failed to drop replica ${region} on ${tableName}: ${replicaCleanupErr instanceof Error ? replicaCleanupErr.message : String(replicaCleanupErr)}. Run: aws dynamodb update-table --table-name ${tableName} --replica-updates 'Delete={RegionName=${region}}' --region ${currentRegion}`);
17596
17690
  }
17597
17691
  }
17598
17692
  await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: tableName }));
17599
17693
  } catch (cleanupErr) {
17600
- const cleanupMsg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
17601
- this.logger.warn(`Partial-create cleanup failed for ${tableName}: ${cleanupMsg}. Run: aws dynamodb delete-table --table-name ${tableName} to remove the orphaned AWS-side table.`);
17694
+ warn(`Partial-create cleanup failed for ${tableName}: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}. Run: aws dynamodb delete-table --table-name ${tableName} to remove the orphaned AWS-side table.`);
17602
17695
  }
17603
17696
  const cause = wiringError instanceof Error ? wiringError : void 0;
17604
17697
  throw new ProvisioningError(`Failed to create DynamoDB GlobalTable ${logicalId}: ${wiringError instanceof Error ? wiringError.message : String(wiringError)}`, resourceType, logicalId, tableName, cause);
@@ -17613,13 +17706,14 @@ var DynamoDBGlobalTableProvider = class {
17613
17706
  * useless. Kept as ONE formatter so the five translation call sites cannot
17614
17707
  * word the same defect differently.
17615
17708
  */
17616
- reportThroughputDiagnostics(diagnostics, logicalId, physicalId) {
17709
+ reportThroughputDiagnostics(diagnostics, logicalId, physicalId, maskSecrets = (text) => text) {
17710
+ const warn = (message) => this.logger.warn(maskSecrets(message));
17617
17711
  const seen = /* @__PURE__ */ new Set();
17618
17712
  for (const diagnostic of diagnostics) {
17619
- const line = `DynamoDB GlobalTable ${logicalId}: ${diagnostic.indexName ? `GSI '${diagnostic.indexName}' on ${physicalId}` : physicalId}: ${diagnostic.message}`;
17713
+ const line = `DynamoDB GlobalTable ${logicalId}: ${diagnostic.indexName ? `GSI '${maskSecrets(diagnostic.indexName)}' on ${physicalId}` : physicalId}: ${diagnostic.message}`;
17620
17714
  if (seen.has(line)) continue;
17621
17715
  seen.add(line);
17622
- this.logger.warn(line);
17716
+ warn(line);
17623
17717
  }
17624
17718
  }
17625
17719
  /**
@@ -17644,12 +17738,13 @@ var DynamoDBGlobalTableProvider = class {
17644
17738
  * there is no template edit that "fixes" it other than raising a number the
17645
17739
  * user never chose.
17646
17740
  */
17647
- warmThroughputForSend(indexName, desired, liveWarmByIndexName, logicalId, physicalId) {
17741
+ warmThroughputForSend(indexName, desired, liveWarmByIndexName, logicalId, physicalId, maskSecrets = (text) => text) {
17742
+ const warn = (message) => this.logger.warn(maskSecrets(message));
17648
17743
  if (!desired) return void 0;
17649
17744
  const live = liveWarmByIndexName.get(indexName);
17650
17745
  if (!isWarmThroughputDecrease(desired, live)) return desired;
17651
17746
  const format = (spec) => WARM_THROUGHPUT_MEMBERS.filter((m) => spec?.[m] !== void 0).map((m) => `${m}=${spec?.[m]}`).join(", ");
17652
- this.logger.warn(`DynamoDB GlobalTable ${logicalId}: GSI '${indexName}' on ${physicalId}: WarmThroughput was NOT sent — the template asks for ${format(desired)} while AWS reports ${format(live)}, and DynamoDB rejects a decrease ("decreasing WarmThroughput is not supported"). Warm throughput only rises with a table's traffic, so a template below the live value can never be applied; raise it to at least the live value, or remove it. Every other change to this index is applied normally.`);
17747
+ warn(`DynamoDB GlobalTable ${logicalId}: GSI '${maskSecrets(indexName)}' on ${physicalId}: WarmThroughput was NOT sent — the template asks for ${format(desired)} while AWS reports ${format(live)}, and DynamoDB rejects a decrease ("decreasing WarmThroughput is not supported"). Warm throughput only rises with a table's traffic, so a template below the live value can never be applied; raise it to at least the live value, or remove it. Every other change to this index is applied normally.`);
17653
17748
  }
17654
17749
  /**
17655
17750
  * Add a single replica region. Issues one `UpdateTableCommand` with
@@ -17717,8 +17812,11 @@ var DynamoDBGlobalTableProvider = class {
17717
17812
  * should catch these as REPLACEMENT before ever calling `update()`,
17718
17813
  * but the guard is defense-in-depth.
17719
17814
  */
17720
- async update(logicalId, physicalId, resourceType, properties, previousProperties) {
17721
- this.logger.debug(`Updating DynamoDB GlobalTable ${logicalId}: ${physicalId}`);
17815
+ async update(logicalId, physicalId, resourceType, properties, previousProperties, context) {
17816
+ const maskSecrets = context?.maskSecrets ?? ((text) => text);
17817
+ const warn = (message) => this.logger.warn(maskSecrets(message));
17818
+ const debug = (message) => this.logger.debug(maskSecrets(message));
17819
+ debug(`Updating DynamoDB GlobalTable ${logicalId}: ${maskSecrets(physicalId)}`);
17722
17820
  if (properties["TableName"] !== void 0 && previousProperties["TableName"] !== void 0 && properties["TableName"] !== previousProperties["TableName"]) throw new ProvisioningError(`TableName is immutable on AWS::DynamoDB::GlobalTable; replacement required (deploy with --replace, or destroy + redeploy)`, resourceType, logicalId, physicalId);
17723
17821
  if (properties["KeySchema"] !== void 0 && previousProperties["KeySchema"] !== void 0 && !deepEqual$1(properties["KeySchema"], previousProperties["KeySchema"])) throw new ProvisioningError(`KeySchema is immutable on AWS::DynamoDB::GlobalTable; replacement required (deploy with --replace, or destroy + redeploy)`, resourceType, logicalId, physicalId);
17724
17822
  if (properties["LocalSecondaryIndexes"] !== void 0 && previousProperties["LocalSecondaryIndexes"] !== void 0 && !deepEqual$1(properties["LocalSecondaryIndexes"], previousProperties["LocalSecondaryIndexes"])) throw new ProvisioningError(`LocalSecondaryIndexes is immutable on AWS::DynamoDB::GlobalTable; replacement required (deploy with --replace, or destroy + redeploy)`, resourceType, logicalId, physicalId);
@@ -17730,7 +17828,7 @@ var DynamoDBGlobalTableProvider = class {
17730
17828
  const currentRegion = await this.dynamoDBClient.config.region() ?? "";
17731
17829
  let effectiveProperties;
17732
17830
  try {
17733
- await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
17831
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId, void 0, void 0, maskSecrets);
17734
17832
  const describeResp = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }));
17735
17833
  const tableArn = describeResp.Table?.TableArn;
17736
17834
  const extractLocalTags = (props) => {
@@ -17756,12 +17854,12 @@ var DynamoDBGlobalTableProvider = class {
17756
17854
  const desiredDeclaresBillingMode = properties["BillingMode"] !== void 0;
17757
17855
  const seedAbsentFromLive = recordedOldBillingAbsent && desiredDeclaresBillingMode;
17758
17856
  const oldBilling = recordedOldBillingUsable ? recordedOldBilling : recordedOldBillingAbsent ? seedAbsentFromLive ? liveBillingMode : "PAY_PER_REQUEST" : liveBillingMode;
17759
- if (!recordedOldBillingUsable && !recordedOldBillingAbsent) this.logger.warn(`AWS::DynamoDB::GlobalTable ${logicalId}: the recorded previous BillingMode is unusable (${JSON.stringify(recordedOldBilling)}) — using the table's actual billing mode (${oldBilling}) as the comparison baseline for this update so a corrected template does not issue a same-mode UpdateTable.`);
17760
- if (seedAbsentFromLive && oldBilling !== "PAY_PER_REQUEST") this.logger.warn(`AWS::DynamoDB::GlobalTable ${logicalId}: the cdkd state record declares no BillingMode — using ${oldBilling} as the comparison baseline for this update (${reportedBillingMode !== void 0 ? "the mode DescribeTable reports" : "inferred: DescribeTable reports no BillingModeSummary, which means the table was created without an explicit mode"}), so a template declaring a different mode still applies the flip instead of comparing equal to the create-path default.`);
17857
+ if (!recordedOldBillingUsable && !recordedOldBillingAbsent) warn(`AWS::DynamoDB::GlobalTable ${logicalId}: the recorded previous BillingMode is unusable (${JSON.stringify(maskLeafValue(recordedOldBilling, maskSecrets))}) — using the table's actual billing mode (${oldBilling}) as the comparison baseline for this update so a corrected template does not issue a same-mode UpdateTable.`);
17858
+ if (seedAbsentFromLive && oldBilling !== "PAY_PER_REQUEST") warn(`AWS::DynamoDB::GlobalTable ${logicalId}: the cdkd state record declares no BillingMode — using ${oldBilling} as the comparison baseline for this update (${reportedBillingMode !== void 0 ? "the mode DescribeTable reports" : "inferred: DescribeTable reports no BillingModeSummary, which means the table was created without an explicit mode"}), so a template declaring a different mode still applies the flip instead of comparing equal to the create-path default.`);
17761
17859
  let billingUnusable = false;
17762
17860
  const requestedBilling = requireConfigString(properties["BillingMode"], "PAY_PER_REQUEST", "AWS::DynamoDB::GlobalTable BillingMode", { onUnusable: (message) => {
17763
17861
  billingUnusable = true;
17764
- this.logger.warn(`AWS::DynamoDB::GlobalTable ${logicalId}: ${message} The mode this update compared against (${oldBilling}) is kept for this update rather than flipped to the default.`);
17862
+ warn(`AWS::DynamoDB::GlobalTable ${logicalId}: ${message} The mode this update compared against (${oldBilling}) is kept for this update rather than flipped to the default.`);
17765
17863
  } });
17766
17864
  const newBilling = billingUnusable ? oldBilling : requestedBilling;
17767
17865
  const onDemandCeilingLive = oldBilling === "PAY_PER_REQUEST" && newBilling === "PAY_PER_REQUEST";
@@ -17772,12 +17870,12 @@ var DynamoDBGlobalTableProvider = class {
17772
17870
  let desiredGsiUnusable = false;
17773
17871
  const onUnusableGsiBlock = (message) => {
17774
17872
  desiredGsiUnusable = true;
17775
- this.logger.warn(`AWS::DynamoDB::GlobalTable ${logicalId}: ${message} No GlobalSecondaryIndexes change is applied by this update; the table's existing indexes are left untouched.`);
17873
+ warn(`AWS::DynamoDB::GlobalTable ${logicalId}: ${message} No GlobalSecondaryIndexes change is applied by this update; the table's existing indexes are left untouched.`);
17776
17874
  };
17777
17875
  let previousGsiUnusable = false;
17778
17876
  const onUnusablePreviousGsiBlock = (message) => {
17779
17877
  previousGsiUnusable = true;
17780
- this.logger.warn(`AWS::DynamoDB::GlobalTable ${logicalId}: ${message} (recorded in cdkd state, not in the template) — using the table's LIVE indexes as the comparison baseline for this update, so a corrected template still applies.`);
17878
+ warn(`AWS::DynamoDB::GlobalTable ${logicalId}: ${message} (recorded in cdkd state, not in the template) — using the table's LIVE indexes as the comparison baseline for this update, so a corrected template still applies.`);
17781
17879
  };
17782
17880
  const liveIndexNames = new Set((describeResp.Table?.GlobalSecondaryIndexes ?? []).map((live) => live.IndexName).filter((name) => typeof name === "string"));
17783
17881
  const desiredSdkIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling, "min", flippingToProvisioned ? void 0 : diagnostics, onUnusableGsiBlock);
@@ -17788,14 +17886,14 @@ var DynamoDBGlobalTableProvider = class {
17788
17886
  toSdkReplicaGlobalSecondaryIndexes(replica["GlobalSecondaryIndexes"], newBilling, diagnostics);
17789
17887
  toSdkReplicaThroughputOverrides(replica, newBilling, diagnostics, {});
17790
17888
  }
17791
- this.reportThroughputDiagnostics(diagnostics, logicalId, physicalId);
17889
+ this.reportThroughputDiagnostics(diagnostics, logicalId, physicalId, maskSecrets);
17792
17890
  const newDpe = extractLocalDeletionProtection(properties, currentRegion);
17793
17891
  const oldDpe = extractLocalDeletionProtection(previousProperties, currentRegion);
17794
17892
  const awsDpe = describeResp.Table?.DeletionProtectionEnabled;
17795
17893
  const flatUpdate = { TableName: physicalId };
17796
17894
  let flatChanged = false;
17797
17895
  if (newDpe !== oldDpe || newDpe !== void 0 && typeof awsDpe === "boolean" && Boolean(newDpe) !== awsDpe) {
17798
- if ((oldDpe === true || awsDpe === true) && Boolean(newDpe ?? false) === false && !(newDpe !== void 0)) this.logger.warn(`Auto-disabling DeletionProtectionEnabled on ${physicalId}: the CDK code does not set 'deletionProtection: true' but AWS has it enabled. AWS will accept DeleteTable on this resource after this deploy. If you meant to keep protection on, set 'deletionProtection: true' in your CDK code; if you meant to disable it explicitly, set 'deletionProtection: false' to silence this warning.`);
17896
+ if ((oldDpe === true || awsDpe === true) && Boolean(newDpe ?? false) === false && !(newDpe !== void 0)) warn(`Auto-disabling DeletionProtectionEnabled on ${physicalId}: the CDK code does not set 'deletionProtection: true' but AWS has it enabled. AWS will accept DeleteTable on this resource after this deploy. If you meant to keep protection on, set 'deletionProtection: true' in your CDK code; if you meant to disable it explicitly, set 'deletionProtection: false' to silence this warning.`);
17799
17897
  flatUpdate.DeletionProtectionEnabled = Boolean(newDpe ?? false);
17800
17898
  flatChanged = true;
17801
17899
  }
@@ -17815,7 +17913,7 @@ var DynamoDBGlobalTableProvider = class {
17815
17913
  let streamSpecUnusable = false;
17816
17914
  const streamViewType = readConfigString(properties["StreamSpecification"], "StreamViewType", "NEW_AND_OLD_IMAGES", "AWS::DynamoDB::GlobalTable StreamSpecification", { onUnusable: (message) => {
17817
17915
  streamSpecUnusable = true;
17818
- this.logger.warn(`AWS::DynamoDB::GlobalTable ${logicalId}: ${message} The table's existing stream configuration is left untouched for this update rather than re-pointed at the default.`);
17916
+ warn(`AWS::DynamoDB::GlobalTable ${logicalId}: ${message} The table's existing stream configuration is left untouched for this update rather than re-pointed at the default.`);
17819
17917
  } });
17820
17918
  if (!streamSpecUnusable) {
17821
17919
  flatUpdate.StreamSpecification = {
@@ -17867,7 +17965,7 @@ var DynamoDBGlobalTableProvider = class {
17867
17965
  }
17868
17966
  if (flatChanged) {
17869
17967
  await this.dynamoDBClient.send(new UpdateTableCommand(flatUpdate));
17870
- await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
17968
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId, void 0, void 0, maskSecrets);
17871
17969
  }
17872
17970
  const gsiHandledByBillingFlip = /* @__PURE__ */ new Set();
17873
17971
  if (oldBilling !== newBilling) {
@@ -17896,7 +17994,7 @@ var DynamoDBGlobalTableProvider = class {
17896
17994
  const gsi = byIndexName.get(indexName);
17897
17995
  if (!gsi?.ProvisionedThroughput) continue;
17898
17996
  const previousWarm = previousByIndexName.get(indexName)?.WarmThroughput;
17899
- const warmToSend = gsi.WarmThroughput !== void 0 && !deepEqual$1(gsi.WarmThroughput, previousWarm) ? this.warmThroughputForSend(indexName, gsi.WarmThroughput, liveWarmByIndexName, logicalId, physicalId) : void 0;
17997
+ const warmToSend = gsi.WarmThroughput !== void 0 && !deepEqual$1(gsi.WarmThroughput, previousWarm) ? this.warmThroughputForSend(indexName, gsi.WarmThroughput, liveWarmByIndexName, logicalId, physicalId, maskSecrets) : void 0;
17900
17998
  indexUpdates.push({ Update: {
17901
17999
  IndexName: indexName,
17902
18000
  ProvisionedThroughput: gsi.ProvisionedThroughput,
@@ -17907,21 +18005,21 @@ var DynamoDBGlobalTableProvider = class {
17907
18005
  if (indexUpdates.length > 0) billingUpdate.GlobalSecondaryIndexUpdates = indexUpdates;
17908
18006
  }
17909
18007
  await this.dynamoDBClient.send(new UpdateTableCommand(billingUpdate));
17910
- await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
18008
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId, void 0, void 0, maskSecrets);
17911
18009
  }
17912
18010
  if (deferCeilingsPastFlip) {
17913
18011
  await this.dynamoDBClient.send(new UpdateTableCommand({
17914
18012
  TableName: physicalId,
17915
18013
  OnDemandThroughput: onDemandPayload
17916
18014
  }));
17917
- await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
18015
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId, void 0, void 0, maskSecrets);
17918
18016
  }
17919
18017
  const writeAutoScalingOld = (previousProperties["WriteProvisionedThroughputSettings"] ?? {})["WriteCapacityAutoScalingSettings"];
17920
18018
  const writeAutoScalingNew = (properties["WriteProvisionedThroughputSettings"] ?? {})["WriteCapacityAutoScalingSettings"];
17921
18019
  const effectiveNewAutoScaling = oldBilling === "PROVISIONED" && newBilling === "PAY_PER_REQUEST" ? void 0 : writeAutoScalingNew;
17922
18020
  const autoScalingApplied = /* @__PURE__ */ new Set();
17923
18021
  if ((newBilling === "PROVISIONED" || oldBilling === "PROVISIONED") && !deepEqual$1(writeAutoScalingOld, effectiveNewAutoScaling)) {
17924
- await this.applyAutoScalingDiff(physicalId, "dynamodb:table:WriteCapacityUnits", writeAutoScalingOld, effectiveNewAutoScaling);
18022
+ await this.applyAutoScalingDiff(physicalId, "dynamodb:table:WriteCapacityUnits", writeAutoScalingOld, effectiveNewAutoScaling, void 0, void 0, void 0, maskSecrets);
17925
18023
  autoScalingApplied.add(autoScalingTargetKey({
17926
18024
  dimension: "dynamodb:table:WriteCapacityUnits",
17927
18025
  region: currentRegion
@@ -17934,7 +18032,7 @@ var DynamoDBGlobalTableProvider = class {
17934
18032
  const removedReadAutoScaling = (replica["ReadProvisionedThroughputSettings"] ?? {})["ReadCapacityAutoScalingSettings"];
17935
18033
  if (removedReadAutoScaling) {
17936
18034
  const regionalAutoScalingClient = this.getRegionalAutoScalingClient(region);
17937
- await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", removedReadAutoScaling, void 0, regionalAutoScalingClient);
18035
+ await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", removedReadAutoScaling, void 0, regionalAutoScalingClient, void 0, void 0, maskSecrets);
17938
18036
  autoScalingApplied.add(autoScalingTargetKey({
17939
18037
  dimension: "dynamodb:table:ReadCapacityUnits",
17940
18038
  region
@@ -17951,11 +18049,11 @@ var DynamoDBGlobalTableProvider = class {
17951
18049
  if (!region || region === currentRegion) continue;
17952
18050
  await this.addReplica(physicalId, replica, region, logicalId, newBilling);
17953
18051
  const newReplicaTags = replica["Tags"];
17954
- await this.applyCrossRegionReplicaTagsDiff(tableArn, region, void 0, newReplicaTags, physicalId);
18052
+ await this.applyCrossRegionReplicaTagsDiff(tableArn, region, void 0, newReplicaTags, physicalId, maskSecrets);
17955
18053
  const newReadAutoScaling = (replica["ReadProvisionedThroughputSettings"] ?? {})["ReadCapacityAutoScalingSettings"];
17956
18054
  if (newBilling === "PROVISIONED" && newReadAutoScaling) {
17957
18055
  const regionalAutoScalingClient = this.getRegionalAutoScalingClient(region);
17958
- await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", void 0, newReadAutoScaling, regionalAutoScalingClient);
18056
+ await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", void 0, newReadAutoScaling, regionalAutoScalingClient, void 0, void 0, maskSecrets);
17959
18057
  autoScalingApplied.add(autoScalingTargetKey({
17960
18058
  dimension: "dynamodb:table:ReadCapacityUnits",
17961
18059
  region
@@ -17968,13 +18066,13 @@ var DynamoDBGlobalTableProvider = class {
17968
18066
  const oldReplica = (previousProperties["Replicas"] ?? []).find((r) => r["Region"] === region);
17969
18067
  const oldReplicaTags = oldReplica?.["Tags"];
17970
18068
  const newReplicaTags = replica["Tags"];
17971
- await this.applyCrossRegionReplicaTagsDiff(tableArn, region, oldReplicaTags, newReplicaTags, physicalId);
18069
+ await this.applyCrossRegionReplicaTagsDiff(tableArn, region, oldReplicaTags, newReplicaTags, physicalId, maskSecrets);
17972
18070
  const oldReadAutoScaling = (oldReplica?.["ReadProvisionedThroughputSettings"] ?? {})["ReadCapacityAutoScalingSettings"];
17973
18071
  const newReadAutoScaling = (replica["ReadProvisionedThroughputSettings"] ?? {})["ReadCapacityAutoScalingSettings"];
17974
18072
  const effectiveNewReadAutoScaling = newBilling === "PAY_PER_REQUEST" ? void 0 : newReadAutoScaling;
17975
18073
  if ((newBilling === "PROVISIONED" || oldBilling === "PROVISIONED") && !deepEqual$1(oldReadAutoScaling, effectiveNewReadAutoScaling)) {
17976
18074
  const regionalAutoScalingClient = this.getRegionalAutoScalingClient(region);
17977
- await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", oldReadAutoScaling, effectiveNewReadAutoScaling, regionalAutoScalingClient);
18075
+ await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", oldReadAutoScaling, effectiveNewReadAutoScaling, regionalAutoScalingClient, void 0, void 0, maskSecrets);
17978
18076
  autoScalingApplied.add(autoScalingTargetKey({
17979
18077
  dimension: "dynamodb:table:ReadCapacityUnits",
17980
18078
  region
@@ -17992,7 +18090,7 @@ var DynamoDBGlobalTableProvider = class {
17992
18090
  const droppedOverrides = [];
17993
18091
  if (oldReplicaOverrides.OnDemandThroughputOverride && !replicaOverrides.OnDemandThroughputOverride) droppedOverrides.push("ReadOnDemandThroughputSettings");
17994
18092
  if (oldReplicaOverrides.ProvisionedThroughputOverride && !replicaOverrides.ProvisionedThroughputOverride) droppedOverrides.push("ReadProvisionedThroughputSettings");
17995
- if (droppedOverrides.length > 0) this.logger.warn(`Cross-region replica ${region} of ${physicalId}: the template no longer declares ${droppedOverrides.join(" / ")}, but DynamoDB offers no way to CLEAR a replica-level throughput override — a -1 sentinel is stored literally, and an empty override block wedges the table in UPDATING (both live-probed, issue #1436). The previous override is STILL IN EFFECT on AWS. Set an explicit value, or remove and re-add the replica, to change it.`);
18093
+ if (droppedOverrides.length > 0) warn(`Cross-region replica ${region} of ${physicalId}: the template no longer declares ${droppedOverrides.map((o) => maskSecrets(o)).join(" / ")}, but DynamoDB offers no way to CLEAR a replica-level throughput override — a -1 sentinel is stored literally, and an empty override block wedges the table in UPDATING (both live-probed, issue #1436). The previous override is STILL IN EFFECT on AWS. Set an explicit value, or remove and re-add the replica, to change it.`);
17996
18094
  }
17997
18095
  if (replica["TableClassOverride"]) updateAction.TableClassOverride = replica["TableClassOverride"];
17998
18096
  if (!(updateAction.KMSMasterKeyId !== void 0 || updateAction.GlobalSecondaryIndexes !== void 0 || updateAction.TableClassOverride !== void 0 || updateAction.ProvisionedThroughputOverride !== void 0 || updateAction.OnDemandThroughputOverride !== void 0)) {
@@ -18014,7 +18112,7 @@ var DynamoDBGlobalTableProvider = class {
18014
18112
  if (previousGsiUnusable) {
18015
18113
  const desiredNames = new Set(desiredSdkIndexes.map((gsi) => gsi.IndexName).filter((name) => typeof name === "string"));
18016
18114
  const liveOnly = [...liveIndexNames].filter((name) => !desiredNames.has(name));
18017
- if (liveOnly.length > 0) this.logger.warn(`AWS::DynamoDB::GlobalTable ${logicalId}: ${liveOnly.join(", ")} exist(s) on the table but not in the template. No Delete is issued while the recorded GlobalSecondaryIndexes are unusable — a junk state record cannot prove cdkd created them, and an index delete is irreversible. This does NOT self-heal, and no later cdkd deploy will remove them: delete the index directly, e.g. aws dynamodb update-table --table-name ${physicalId} --global-secondary-index-updates '[{"Delete":{"IndexName":"<name>"}}]'.`);
18115
+ if (liveOnly.length > 0) warn(`AWS::DynamoDB::GlobalTable ${logicalId}: ${liveOnly.map((n) => maskSecrets(n)).join(", ")} exist(s) on the table but not in the template. No Delete is issued while the recorded GlobalSecondaryIndexes are unusable — a junk state record cannot prove cdkd created them, and an index delete is irreversible. This does NOT self-heal, and no later cdkd deploy will remove them: delete the index directly, e.g. aws dynamodb update-table --table-name ${physicalId} --global-secondary-index-updates '[{"Delete":{"IndexName":"<name>"}}]'.`);
18018
18116
  }
18019
18117
  const gsiDiff = desiredGsiUnusable ? {
18020
18118
  added: [],
@@ -18031,11 +18129,11 @@ var DynamoDBGlobalTableProvider = class {
18031
18129
  TableName: physicalId,
18032
18130
  GlobalSecondaryIndexUpdates: [gsiUpdate]
18033
18131
  }));
18034
- await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
18132
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId, void 0, void 0, maskSecrets);
18035
18133
  }
18036
18134
  for (const gsi of gsiDiff.added) {
18037
18135
  if (!gsi.IndexName || !gsi.KeySchema || !gsi.Projection) continue;
18038
- const addedWarmToSend = this.warmThroughputForSend(gsi.IndexName, gsi.WarmThroughput, liveWarmByIndexName, logicalId, physicalId);
18136
+ const addedWarmToSend = this.warmThroughputForSend(gsi.IndexName, gsi.WarmThroughput, liveWarmByIndexName, logicalId, physicalId, maskSecrets);
18039
18137
  const gsiUpdate = { Create: {
18040
18138
  IndexName: gsi.IndexName,
18041
18139
  KeySchema: gsi.KeySchema,
@@ -18049,7 +18147,7 @@ var DynamoDBGlobalTableProvider = class {
18049
18147
  AttributeDefinitions: newAttrs,
18050
18148
  GlobalSecondaryIndexUpdates: [gsiUpdate]
18051
18149
  }));
18052
- await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
18150
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId, void 0, void 0, maskSecrets);
18053
18151
  }
18054
18152
  for (const gsi of gsiDiff.modified) {
18055
18153
  if (typeof gsi.IndexName !== "string" || gsi.IndexName.length === 0) continue;
@@ -18071,12 +18169,12 @@ var DynamoDBGlobalTableProvider = class {
18071
18169
  const previousSdk = previousSdkByName.get(gsi.IndexName);
18072
18170
  let warmDecreaseRefused = false;
18073
18171
  if (gsi.WarmThroughput !== void 0 && !deepEqual$1(gsi.WarmThroughput, previousSdk?.WarmThroughput)) {
18074
- const warmToSend = this.warmThroughputForSend(gsi.IndexName, gsi.WarmThroughput, liveWarmByIndexName, logicalId, physicalId);
18172
+ const warmToSend = this.warmThroughputForSend(gsi.IndexName, gsi.WarmThroughput, liveWarmByIndexName, logicalId, physicalId, maskSecrets);
18075
18173
  if (warmToSend) update.WarmThroughput = warmToSend;
18076
18174
  else warmDecreaseRefused = true;
18077
18175
  }
18078
18176
  if (!update.ProvisionedThroughput && !update.OnDemandThroughput && !update.WarmThroughput) {
18079
- if (!billingFlipped && unresolvedMembers.length === 0 && !warmDecreaseRefused) this.logger.warn(`GSI '${gsi.IndexName}' on ${physicalId} changed in a way DynamoDB's UpdateTable cannot express (KeySchema / Projection are immutable on an existing index). Recreate the index under a new name to apply the change.`);
18177
+ if (!billingFlipped && unresolvedMembers.length === 0 && !warmDecreaseRefused) warn(`GSI '${maskSecrets(gsi.IndexName ?? "")}' on ${physicalId} changed in a way DynamoDB's UpdateTable cannot express (KeySchema / Projection are immutable on an existing index). Recreate the index under a new name to apply the change.`);
18080
18178
  continue;
18081
18179
  }
18082
18180
  const gsiUpdate = { Update: update };
@@ -18084,14 +18182,14 @@ var DynamoDBGlobalTableProvider = class {
18084
18182
  TableName: physicalId,
18085
18183
  GlobalSecondaryIndexUpdates: [gsiUpdate]
18086
18184
  }));
18087
- await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
18185
+ await this.waitForTableActiveAfterUpdate(physicalId, logicalId, void 0, void 0, maskSecrets);
18088
18186
  }
18089
18187
  const skipAutoScalingForUnusableGsi = desiredGsiUnusable && newBilling !== "PAY_PER_REQUEST";
18090
18188
  const autoScalingMeaningfulForIndexes = (newBilling === "PROVISIONED" || oldBilling === "PROVISIONED") && !skipAutoScalingForUnusableGsi;
18091
- if (skipAutoScalingForUnusableGsi && oldBilling === "PROVISIONED") this.logger.warn(`AWS::DynamoDB::GlobalTable ${logicalId}: skipping the auto-scaling target reconcile because the GlobalSecondaryIndexes block is unusable — the per-index targets are derived from it, so reconciling would deregister the live indexes' scalable targets. Existing targets are left untouched.`);
18189
+ if (skipAutoScalingForUnusableGsi && oldBilling === "PROVISIONED") warn(`AWS::DynamoDB::GlobalTable ${logicalId}: skipping the auto-scaling target reconcile because the GlobalSecondaryIndexes block is unusable — the per-index targets are derived from it, so reconciling would deregister the live indexes' scalable targets. Existing targets are left untouched.`);
18092
18190
  if (autoScalingMeaningfulForIndexes) {
18093
18191
  if (gsiDiff.added.length > 0) await this.waitForIndexesActive(physicalId, logicalId);
18094
- await this.reconcileAutoScalingTargets(physicalId, collectAutoScalingTargets(previousProperties, currentRegion), newBilling === "PAY_PER_REQUEST" ? [] : collectAutoScalingTargets(properties, currentRegion), currentRegion, autoScalingApplied);
18192
+ await this.reconcileAutoScalingTargets(physicalId, collectAutoScalingTargets(previousProperties, currentRegion), newBilling === "PAY_PER_REQUEST" ? [] : collectAutoScalingTargets(properties, currentRegion), currentRegion, autoScalingApplied, maskSecrets);
18095
18193
  }
18096
18194
  if (!deepEqual$1(properties["TimeToLiveSpecification"], previousProperties["TimeToLiveSpecification"])) {
18097
18195
  const sendTtl = async (cmd) => {
@@ -18172,22 +18270,23 @@ var DynamoDBGlobalTableProvider = class {
18172
18270
  * No-op when `oldTags` deep-equals `newTags` — the caller is allowed
18173
18271
  * to invoke unconditionally without first diffing.
18174
18272
  */
18175
- async applyCrossRegionReplicaTagsDiff(tableArn, region, oldTags, newTags, physicalIdForLogs) {
18273
+ async applyCrossRegionReplicaTagsDiff(tableArn, region, oldTags, newTags, physicalIdForLogs, maskSecrets = (text) => text) {
18274
+ const warn = (message) => this.logger.warn(maskSecrets(message));
18176
18275
  if (deepEqual$1(oldTags, newTags)) return;
18177
18276
  if (!tableArn) {
18178
- this.logger.warn(`Local DescribeTable returned no TableArn — cannot propagate Tags to cross-region replica ${region} of ${physicalIdForLogs}`);
18277
+ warn(`Local DescribeTable returned no TableArn — cannot propagate Tags to cross-region replica ${region} of ${physicalIdForLogs}`);
18179
18278
  return;
18180
18279
  }
18181
18280
  const replicaArn = this.replicaArnForRegion(tableArn, region);
18182
18281
  if (!replicaArn) {
18183
- this.logger.warn(`Could not derive replica ARN for region ${region} from ${tableArn} — skipping Tags propagation for ${physicalIdForLogs}`);
18282
+ warn(`Could not derive replica ARN for region ${region} from ${tableArn} — skipping Tags propagation for ${physicalIdForLogs}`);
18184
18283
  return;
18185
18284
  }
18186
18285
  try {
18187
18286
  const regionalClient = this.getRegionalClient(region);
18188
18287
  await this.applyTagDiffOnClient(regionalClient, replicaArn, oldTags, newTags);
18189
18288
  } catch (tagErr) {
18190
- this.logger.warn(`Could not apply Tags diff to cross-region replica ${region} of ${physicalIdForLogs}: ${tagErr instanceof Error ? tagErr.message : String(tagErr)}. The replica's Tags state will surface as drift until the next successful deploy.`);
18289
+ warn(`Could not apply Tags diff to cross-region replica ${region} of ${physicalIdForLogs}: ${tagErr instanceof Error ? tagErr.message : String(tagErr)}. The replica's Tags state will surface as drift until the next successful deploy.`);
18191
18290
  }
18192
18291
  }
18193
18292
  /**
@@ -18264,7 +18363,8 @@ var DynamoDBGlobalTableProvider = class {
18264
18363
  * create-only field with no corresponding `RegisterScalableTarget`
18265
18364
  * surface; `readAutoScalingSettings` also explicitly skips it.
18266
18365
  */
18267
- async applyAutoScalingDiff(tableName, dimension, oldSettings, newSettings, client, indexName, budget) {
18366
+ async applyAutoScalingDiff(tableName, dimension, oldSettings, newSettings, client, indexName, budget, maskSecrets = (text) => text) {
18367
+ const warn = (message) => this.logger.warn(maskSecrets(message));
18268
18368
  const asMaxRetries = autoScalingRetriesWithinDeleteBudget(budget, 8);
18269
18369
  const metricType = dimension.endsWith("WriteCapacityUnits") ? "DynamoDBWriteCapacityUtilization" : "DynamoDBReadCapacityUtilization";
18270
18370
  const resourceId = autoScalingResourceId(tableName, indexName);
@@ -18277,7 +18377,7 @@ var DynamoDBGlobalTableProvider = class {
18277
18377
  const minCapacity = Number(newSettings["MinCapacity"] ?? 0);
18278
18378
  const maxCapacity = Number(newSettings["MaxCapacity"] ?? 0);
18279
18379
  if (!Number.isFinite(minCapacity) || !Number.isFinite(maxCapacity)) {
18280
- this.logger.warn(`Cannot apply auto-scaling diff on ${tableName} (${dimension}): MinCapacity / MaxCapacity must be numbers, got ${String(newSettings["MinCapacity"])} / ${String(newSettings["MaxCapacity"])}`);
18380
+ warn(`Cannot apply auto-scaling diff on ${tableName} (${dimension}): MinCapacity / MaxCapacity must be numbers, got ${maskSecrets(String(newSettings["MinCapacity"]))} / ${maskSecrets(String(newSettings["MaxCapacity"]))}`);
18281
18381
  return;
18282
18382
  }
18283
18383
  try {
@@ -18293,13 +18393,13 @@ var DynamoDBGlobalTableProvider = class {
18293
18393
  ...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
18294
18394
  });
18295
18395
  } catch (err) {
18296
- this.logger.warn(`Could not register auto-scaling target on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling register-scalable-target --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension} --min-capacity ${minCapacity} --max-capacity ${maxCapacity}`);
18396
+ warn(`Could not register auto-scaling target on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling register-scalable-target --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension} --min-capacity ${minCapacity} --max-capacity ${maxCapacity}`);
18297
18397
  return;
18298
18398
  }
18299
18399
  const tttCfg = newSettings["TargetTrackingScalingPolicyConfiguration"] ?? {};
18300
18400
  const targetValue = Number(tttCfg["TargetValue"]);
18301
18401
  if (!Number.isFinite(targetValue)) {
18302
- this.logger.warn(`Auto-scaling target registered on ${tableName} (${dimension}) but TargetValue is missing or non-numeric — skipping PutScalingPolicy. Provide TargetTrackingScalingPolicyConfiguration.TargetValue in the template.`);
18402
+ warn(`Auto-scaling target registered on ${tableName} (${dimension}) but TargetValue is missing or non-numeric — skipping PutScalingPolicy. Provide TargetTrackingScalingPolicyConfiguration.TargetValue in the template.`);
18303
18403
  return;
18304
18404
  }
18305
18405
  const targetTrackingConfig = {
@@ -18324,7 +18424,7 @@ var DynamoDBGlobalTableProvider = class {
18324
18424
  });
18325
18425
  this.logger.debug(`Upserted auto-scaling policy ${policyName} on ${tableName} (${dimension})`);
18326
18426
  } catch (err) {
18327
- this.logger.warn(`Could not put auto-scaling policy on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling put-scaling-policy --policy-name ${policyName} --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension} --policy-type TargetTrackingScaling`);
18427
+ warn(`Could not put auto-scaling policy on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling put-scaling-policy --policy-name ${policyName} --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension} --policy-type TargetTrackingScaling`);
18328
18428
  }
18329
18429
  return;
18330
18430
  }
@@ -18346,7 +18446,7 @@ var DynamoDBGlobalTableProvider = class {
18346
18446
  ...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
18347
18447
  });
18348
18448
  } catch (err) {
18349
- if (!isObjectNotFound(err)) this.logger.warn(`Could not delete auto-scaling policy on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling delete-scaling-policy --policy-name ${policyName} --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension}`);
18449
+ if (!isObjectNotFound(err)) warn(`Could not delete auto-scaling policy on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling delete-scaling-policy --policy-name ${policyName} --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension}`);
18350
18450
  }
18351
18451
  try {
18352
18452
  await withRetry(() => asClient.send(new DeregisterScalableTargetCommand({
@@ -18360,7 +18460,7 @@ var DynamoDBGlobalTableProvider = class {
18360
18460
  });
18361
18461
  this.logger.debug(`Deregistered auto-scaling target ${resourceId} (${dimension})`);
18362
18462
  } catch (err) {
18363
- if (!isObjectNotFound(err)) this.logger.warn(`Could not deregister auto-scaling target on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling deregister-scalable-target --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension}`);
18463
+ if (!isObjectNotFound(err)) warn(`Could not deregister auto-scaling target on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling deregister-scalable-target --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension}`);
18364
18464
  }
18365
18465
  }
18366
18466
  /**
@@ -18466,7 +18566,7 @@ var DynamoDBGlobalTableProvider = class {
18466
18566
  * skipped. When the probe fails, presence is unknown and everything is
18467
18567
  * upserted — the correct direction to fail.
18468
18568
  */
18469
- async reconcileAutoScalingTargets(tableName, previousSpecs, desiredSpecs, localRegion, alreadyApplied = /* @__PURE__ */ new Set()) {
18569
+ async reconcileAutoScalingTargets(tableName, previousSpecs, desiredSpecs, localRegion, alreadyApplied = /* @__PURE__ */ new Set(), maskSecrets = (text) => text) {
18470
18570
  const previousSpecsToUse = previousSpecs.filter((s) => !alreadyApplied.has(autoScalingTargetKey(s)));
18471
18571
  const desiredSpecsToUse = desiredSpecs.filter((s) => !alreadyApplied.has(autoScalingTargetKey(s)));
18472
18572
  const previousByKey = /* @__PURE__ */ new Map();
@@ -18479,12 +18579,12 @@ var DynamoDBGlobalTableProvider = class {
18479
18579
  const previous = previousByKey.get(key);
18480
18580
  if (registered?.has(key) && previous !== void 0 && deepEqual$1(previous.settings, spec.settings)) continue;
18481
18581
  const client = await this.autoScalingClientForRegion(spec.region, localRegion);
18482
- await this.applyAutoScalingDiff(tableName, spec.dimension, previous?.settings, spec.settings, client, spec.indexName);
18582
+ await this.applyAutoScalingDiff(tableName, spec.dimension, previous?.settings, spec.settings, client, spec.indexName, void 0, maskSecrets);
18483
18583
  }
18484
18584
  for (const [key, spec] of previousByKey) {
18485
18585
  if (handled.has(key)) continue;
18486
18586
  const client = await this.autoScalingClientForRegion(spec.region, localRegion);
18487
- await this.applyAutoScalingDiff(tableName, spec.dimension, spec.settings, void 0, client, spec.indexName);
18587
+ await this.applyAutoScalingDiff(tableName, spec.dimension, spec.settings, void 0, client, spec.indexName, void 0, maskSecrets);
18488
18588
  }
18489
18589
  }
18490
18590
  /**
@@ -19171,7 +19271,7 @@ var DynamoDBGlobalTableProvider = class {
19171
19271
  * rejection as the backstop, which is the behavior it had before #1521 for
19172
19272
  * every case except the one the wait exists to fix.
19173
19273
  */
19174
- async waitForTableActiveAfterUpdate(tableName, logicalId, maxAttempts = 600, budget) {
19274
+ async waitForTableActiveAfterUpdate(tableName, logicalId, maxAttempts = 600, budget, maskSecrets = (text) => text) {
19175
19275
  const wait = beginDeleteWait(maxAttempts, budget);
19176
19276
  let tableReachedActive = false;
19177
19277
  while (wait.nextPoll()) {
@@ -19184,10 +19284,10 @@ var DynamoDBGlobalTableProvider = class {
19184
19284
  }
19185
19285
  const budgetNote = wait.note();
19186
19286
  if (tableReachedActive) {
19187
- this.logger.warn(`Indexes on ${tableName} (${logicalId}) were still transitioning after ${wait.pollsRun}s — a large index backfill can outlive this wait. Proceeding; AWS rejects the next call if it is still too early.${budgetNote}`);
19287
+ this.logger.warn(maskSecrets(`Indexes on ${tableName} (${logicalId}) were still transitioning after ${wait.pollsRun}s — a large index backfill can outlive this wait. Proceeding; AWS rejects the next call if it is still too early.${budgetNote}`));
19188
19288
  return;
19189
19289
  }
19190
- throw new ProvisioningError(`Table ${tableName} did not reach ACTIVE within ${wait.pollsRun}s after UpdateTable${budgetNote}`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
19290
+ throw new ProvisioningError(maskSecrets(`Table ${tableName} did not reach ACTIVE within ${wait.pollsRun}s after UpdateTable${budgetNote}`), "AWS::DynamoDB::GlobalTable", logicalId, tableName);
19191
19291
  }
19192
19292
  /**
19193
19293
  * Wait until every GSI reports `IndexStatus: ACTIVE` (Issue #1419).
@@ -28277,11 +28377,19 @@ var ApiGatewayV2Provider = class {
28277
28377
  if (!this.client) this.client = new ApiGatewayV2Client(this.providerRegion ? { region: this.providerRegion } : {});
28278
28378
  return this.client;
28279
28379
  }
28280
- async create(logicalId, resourceType, properties) {
28380
+ /**
28381
+ * The `context` parameter is read for ONE thing today: `maskSecrets` (issue
28382
+ * #1932 item 3, adopted here by issue #1997). `createIntegration` is the only
28383
+ * arm that interpolates a RESOLVED property value into a log line, so it is
28384
+ * the only arm the masker is threaded into; the rest name nothing but the
28385
+ * logical id. Read defensively — `create()` is also called by the import path
28386
+ * and by tests, neither of which supplies a context.
28387
+ */
28388
+ async create(logicalId, resourceType, properties, context) {
28281
28389
  switch (resourceType) {
28282
28390
  case "AWS::ApiGatewayV2::Api": return this.createApi(logicalId, resourceType, properties);
28283
28391
  case "AWS::ApiGatewayV2::Stage": return this.createStage(logicalId, resourceType, properties);
28284
- case "AWS::ApiGatewayV2::Integration": return this.createIntegration(logicalId, resourceType, properties);
28392
+ case "AWS::ApiGatewayV2::Integration": return this.createIntegration(logicalId, resourceType, properties, context?.maskSecrets);
28285
28393
  case "AWS::ApiGatewayV2::Route": return this.createRoute(logicalId, resourceType, properties);
28286
28394
  case "AWS::ApiGatewayV2::Authorizer": return this.createAuthorizer(logicalId, resourceType, properties);
28287
28395
  default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId);
@@ -28297,12 +28405,19 @@ var ApiGatewayV2Provider = class {
28297
28405
  * on Api; `StageName` on Stage) are not part of the Update input shape
28298
28406
  * and are handled by the deploy engine's immutable-property
28299
28407
  * replacement path.
28408
+ *
28409
+ * The `context` parameter mirrors {@link create}'s: read only for
28410
+ * `maskSecrets`, and only `updateIntegration` needs it. The update path
28411
+ * carries the same RESOLVED bag as the create path, so the masking
28412
+ * requirement is identical on both — a masker present on one and absent from
28413
+ * the other is not a partial fix, it is a fix with a hole in the shape of
28414
+ * whichever path a given deploy takes.
28300
28415
  */
28301
- async update(logicalId, physicalId, resourceType, properties, previousProperties) {
28416
+ async update(logicalId, physicalId, resourceType, properties, previousProperties, context) {
28302
28417
  switch (resourceType) {
28303
28418
  case "AWS::ApiGatewayV2::Api": return this.updateApi(logicalId, physicalId, resourceType, properties, previousProperties);
28304
28419
  case "AWS::ApiGatewayV2::Stage": return this.updateStage(logicalId, physicalId, resourceType, properties, previousProperties);
28305
- case "AWS::ApiGatewayV2::Integration": return this.updateIntegration(logicalId, physicalId, resourceType, properties, previousProperties);
28420
+ case "AWS::ApiGatewayV2::Integration": return this.updateIntegration(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
28306
28421
  case "AWS::ApiGatewayV2::Route": return this.updateRoute(logicalId, physicalId, resourceType, properties, previousProperties);
28307
28422
  case "AWS::ApiGatewayV2::Authorizer": return this.updateAuthorizer(logicalId, physicalId, resourceType, properties, previousProperties);
28308
28423
  default: throw new ResourceUpdateNotSupportedError(resourceType, logicalId, "Unsupported API Gateway V2 resource type for in-place update in cdkd; re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.");
@@ -28433,7 +28548,7 @@ var ApiGatewayV2Provider = class {
28433
28548
  getStageAttribute(physicalId, attributeName) {
28434
28549
  if (attributeName === "StageName") return physicalId;
28435
28550
  }
28436
- async createIntegration(logicalId, resourceType, properties) {
28551
+ async createIntegration(logicalId, resourceType, properties, maskSecrets) {
28437
28552
  this.logger.debug(`Creating API Gateway V2 Integration ${logicalId}`);
28438
28553
  const apiId = properties["ApiId"];
28439
28554
  const integrationType = properties["IntegrationType"];
@@ -28456,7 +28571,7 @@ var ApiGatewayV2Provider = class {
28456
28571
  IntegrationSubtype: properties["IntegrationSubtype"],
28457
28572
  PassthroughBehavior: properties["PassthroughBehavior"],
28458
28573
  RequestTemplates: properties["RequestTemplates"],
28459
- ResponseParameters: this.toSdkResponseParameters(properties["ResponseParameters"]),
28574
+ ResponseParameters: this.toSdkResponseParameters(properties["ResponseParameters"], maskSecrets),
28460
28575
  TemplateSelectionExpression: properties["TemplateSelectionExpression"],
28461
28576
  TlsConfig: properties["TlsConfig"]
28462
28577
  }))).IntegrationId;
@@ -28999,7 +29114,7 @@ var ApiGatewayV2Provider = class {
28999
29114
  * `RequestTemplates` / `ResponseParameters` / `TemplateSelectionExpression`
29000
29115
  * / `TlsConfig`).
29001
29116
  */
29002
- async updateIntegration(logicalId, physicalId, resourceType, properties, previousProperties) {
29117
+ async updateIntegration(logicalId, physicalId, resourceType, properties, previousProperties, maskSecrets) {
29003
29118
  const apiId = properties["ApiId"] ?? previousProperties["ApiId"];
29004
29119
  if (!apiId) throw new ProvisioningError(`ApiId is required to update Integration ${logicalId}`, resourceType, logicalId, physicalId);
29005
29120
  if (properties["ApiId"] !== void 0 && previousProperties["ApiId"] !== void 0 && properties["ApiId"] !== previousProperties["ApiId"]) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, "ApiId is immutable on AWS::ApiGatewayV2::Integration; re-deploy with cdkd deploy --replace");
@@ -29075,7 +29190,7 @@ var ApiGatewayV2Provider = class {
29075
29190
  changed = true;
29076
29191
  }
29077
29192
  if (properties["ResponseParameters"] != null && !this.deepEqual(properties["ResponseParameters"], previousProperties["ResponseParameters"])) {
29078
- input.ResponseParameters = this.toSdkResponseParameters(properties["ResponseParameters"]);
29193
+ input.ResponseParameters = this.toSdkResponseParameters(properties["ResponseParameters"], maskSecrets);
29079
29194
  changed = true;
29080
29195
  }
29081
29196
  if (properties["TemplateSelectionExpression"] != null && properties["TemplateSelectionExpression"] !== previousProperties["TemplateSelectionExpression"]) {
@@ -29475,8 +29590,25 @@ var ApiGatewayV2Provider = class {
29475
29590
  * Skipping it would re-introduce the silent drop this method exists to close.
29476
29591
  * A `Destination` is never coerced — it becomes an SDK map KEY, and a
29477
29592
  * non-string there is a malformed template, not a scalar shorthand.
29478
- */
29479
- toSdkResponseParameters(value) {
29593
+ *
29594
+ * `maskSecrets` is the caller's secret masker (issue #1932 item 3, adopted
29595
+ * here by issue #1997), threaded from `CreateContext` / `UpdateContext` via
29596
+ * `createIntegration` / `updateIntegration`. The warning below names the
29597
+ * offending `Destination` / `Source`, which come out of the RESOLVED
29598
+ * `properties` bag — a `{{resolve:secretsmanager:...}}` scalar is plaintext by
29599
+ * the time a provider sees it, and neither the deploy engine's error text nor
29600
+ * the resolver's debug line covers a provider's own `logger.warn`. It defaults
29601
+ * to IDENTITY so `readCurrentState`'s inverse mapper and every unit test keep
29602
+ * working unchanged.
29603
+ */
29604
+ toSdkResponseParameters(value, maskSecrets = (text) => text) {
29605
+ const warn = (message) => this.logger.warn(maskSecrets(message));
29606
+ const maskLeaf = (value) => {
29607
+ if (typeof value === "string") return maskSecrets(value);
29608
+ if (Array.isArray(value)) return value.map(maskLeaf);
29609
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [maskSecrets(k), maskLeaf(v)]));
29610
+ return value;
29611
+ };
29480
29612
  if (value == null) return void 0;
29481
29613
  if (typeof value !== "object" || Array.isArray(value)) return value;
29482
29614
  const out = {};
@@ -29496,7 +29628,7 @@ var ApiGatewayV2Provider = class {
29496
29628
  const source = entry != null && typeof entry === "object" && !Array.isArray(entry) ? entry["Source"] : void 0;
29497
29629
  const coerced = typeof source === "string" ? source : typeof source === "number" || typeof source === "boolean" ? String(source) : void 0;
29498
29630
  if (typeof destination !== "string" || coerced === void 0) {
29499
- this.logger.warn(`AWS::ApiGatewayV2::Integration ResponseParameters['${statusCode}'] has an entry cdkd cannot deliver (Destination must be a string and Source a string / number / boolean; got ${JSON.stringify(destination)} / ${JSON.stringify(source)}); skipping that entry.`);
29631
+ warn(`AWS::ApiGatewayV2::Integration ResponseParameters['${maskSecrets(statusCode)}'] has an entry cdkd cannot deliver (Destination must be a string and Source a string / number / boolean; got ${JSON.stringify(maskLeaf(destination))} / ${JSON.stringify(maskLeaf(source))}); skipping that entry.`);
29500
29632
  continue;
29501
29633
  }
29502
29634
  mapped[destination] = coerced;
@@ -40034,8 +40166,7 @@ function buildMfaConfigRequest(physicalId, properties, logger, declaredMfaConfig
40034
40166
  const enablesMfaFactor = declaresFactor || request.EmailMfaConfiguration !== void 0;
40035
40167
  request.MfaConfiguration = declaredMfaConfiguration || (enablesMfaFactor ? "OPTIONAL" : "OFF");
40036
40168
  if (enabledMfasBlank) warn(`UserPool ${physicalId}: EnabledMfas is an empty string, which declares no MFA factor and is treated as absent — a mangled Fn::Join or an empty String parameter collapses to this shape. Nothing is enabled from it and MfaConfiguration is ${request.MfaConfiguration}; declare the factor names as a list (e.g. ["${MFA_FACTOR_SOFTWARE_TOKEN}"]) if a factor was intended.`);
40037
- const anyFactorBlock = request.SmsMfaConfiguration !== void 0 || request.SoftwareTokenMfaConfiguration !== void 0 || request.EmailMfaConfiguration !== void 0;
40038
- if (request.MfaConfiguration === "OFF" && anyFactorBlock) warn(`UserPool ${physicalId}: MfaConfiguration is pinned to OFF while an MFA factor block is configured. AWS REJECTS that combination ("Invalid MFA configuration given, can't turn off MFA and configure an MFA together"), so this deploy FAILS rather than producing a pool with MFA disabled. Remove MfaConfiguration (or set it to ON / OPTIONAL) to enable the declared factor.`);
40169
+ const anyFactorBlock = hasAnyMfaFactorBlock(request);
40039
40170
  const maskDeep = (v, depth = 0) => {
40040
40171
  if (typeof v === "string") return maskSecrets(v);
40041
40172
  if (depth >= MASK_WALK_MAX_DEPTH) return v;
@@ -40046,7 +40177,7 @@ function buildMfaConfigRequest(physicalId, properties, logger, declaredMfaConfig
40046
40177
  const dropped = unrecognizedFactors.map((f) => JSON.stringify(maskDeep(f)) ?? String(f));
40047
40178
  if (enabledMfasMalformed) dropped.push(`${JSON.stringify(maskDeep(properties["EnabledMfas"]))} (not a list)`);
40048
40179
  if (dropped.length > 0) {
40049
- const consequence = request.MfaConfiguration === "OFF" && anyFactorBlock ? "MfaConfiguration is OFF while another factor block IS sent, so AWS REJECTS this call outright and the deploy fails" : request.MfaConfiguration === "OFF" ? `MfaConfiguration is OFF, so this pool deploys with MFA DISABLED` : anyFactorBlock ? `MfaConfiguration is ${request.MfaConfiguration} and another factor block IS sent, so these entries are silently ignored rather than failing the call on their own` : `MfaConfiguration is ${request.MfaConfiguration} with no factor enabled, so AWS rejects this call rather than deploying the pool with MFA disabled`;
40180
+ const consequence = request.MfaConfiguration === "OFF" ? `MfaConfiguration is OFF, so this pool deploys with MFA DISABLED` : anyFactorBlock ? `MfaConfiguration is ${request.MfaConfiguration} and another factor block IS sent, so these entries are silently ignored rather than failing the call on their own` : `MfaConfiguration is ${request.MfaConfiguration} with no factor enabled, so AWS rejects this call rather than deploying the pool with MFA disabled`;
40050
40181
  warn(`UserPool ${physicalId}: EnabledMfas entries ${dropped.join(", ")} do not map to an MFA factor block (known: ${MFA_FACTOR_SMS}, ${MFA_FACTOR_SOFTWARE_TOKEN}, ${MFA_FACTOR_EMAIL_OTP}); no factor is enabled from them. ${consequence}.`);
40051
40182
  }
40052
40183
  return request;
@@ -40075,6 +40206,248 @@ function resolveSentMfaConfiguration(properties, declaredMfaConfiguration) {
40075
40206
  return declaredMfaConfiguration || void 0;
40076
40207
  }
40077
40208
  /**
40209
+ * "This `SetUserPoolMfaConfig` request carries an MFA FACTOR sub-block."
40210
+ *
40211
+ * ONE definition, shared by `buildMfaConfigRequest`'s dropped-entry warning and
40212
+ * by the pre-flight refusal below, so the message and the refusal cannot
40213
+ * disagree about the same request. It is precisely
40214
+ * `{Sms, SoftwareToken, Email}MfaConfiguration` -- `WebAuthnConfiguration` is
40215
+ * deliberately NOT a member, because WebAuthn is not an MFA factor and rides
40216
+ * alongside `MfaConfiguration: OFF` on every passkey-only pool (issue #1920).
40217
+ */
40218
+ function hasAnyMfaFactorBlock(request) {
40219
+ return request.SmsMfaConfiguration !== void 0 || request.SoftwareTokenMfaConfiguration !== void 0 || request.EmailMfaConfiguration !== void 0;
40220
+ }
40221
+ /**
40222
+ * The `Policies.SignInPolicy.AllowedFirstAuthFactors` members AWS refuses to
40223
+ * combine with MFA (issue #1975).
40224
+ *
40225
+ * A DENY-list, deliberately, even though the AWS message states an ALLOW-list
40226
+ * ("Only PASSWORD and WEB_AUTHN (if configured) can be enabled as an auth
40227
+ * factor if MFA is enabled"). Written from the allow-list, a member AWS adds
40228
+ * LATER would be refused by cdkd on the day it starts working -- the same
40229
+ * reason the unrecognized-`EnabledMfas` case in `buildMfaConfigRequest` warns
40230
+ * rather than throws, since that set is a hardcoded mirror of an AWS enum. The
40231
+ * cost of the deny-list is the reverse and is the cheaper one: a future
40232
+ * incompatible factor is simply not pre-flighted, which is the pre-#1975
40233
+ * behavior (a loud AWS rejection), not a regression.
40234
+ *
40235
+ * The two members ARE the whole of today's `AuthFactorType` enum outside the
40236
+ * allowed pair (`EMAIL_OTP`, `PASSWORD`, `SMS_OTP`, `WEB_AUTHN` --
40237
+ * `@aws-sdk/client-cognito-identity-provider`), so the deny-list and the
40238
+ * allow-list agree on every value that exists right now.
40239
+ *
40240
+ * BOTH members are MEASURED (us-east-1, 2026-08-19, issue #1975) -- neither is
40241
+ * extrapolated from the other, which is the failure mode this file's own
40242
+ * unrecognized-factor comment warns about:
40243
+ *
40244
+ * [PASSWORD, EMAIL_OTP] + SetUserPoolMfaConfig(ON) -> REJECTED
40245
+ * [PASSWORD, SMS_OTP] + SetUserPoolMfaConfig(ON) -> REJECTED, same message
40246
+ *
40247
+ * The `SMS_OTP` probe needed a precondition worth recording, because without it
40248
+ * the interesting call is never reached: on a pool with NO `SmsConfiguration`,
40249
+ * `CreateUserPool` refuses the factor outright ("SMS_OTP can not be configured
40250
+ * as AuthFactor as user pool is missing valid SMS configuration"), so the probe
40251
+ * had to stand up an SNS-caller IAM role + ExternalId first. That earlier,
40252
+ * different refusal is AWS's own and is NOT something this pre-flight
40253
+ * duplicates.
40254
+ *
40255
+ * Typed `satisfies AuthFactorType[]` rather than left as `string[]`: the values
40256
+ * mirror an SDK enum, so a rename upstream should be a COMPILE error here
40257
+ * rather than a silently-never-matching deny-list.
40258
+ */
40259
+ const MFA_INCOMPATIBLE_FIRST_AUTH_FACTORS = ["EMAIL_OTP", "SMS_OTP"];
40260
+ /**
40261
+ * `Policies.SignInPolicy.AllowedFirstAuthFactors` as a list of STRINGS, or an
40262
+ * empty list when the template declares nothing usable there.
40263
+ *
40264
+ * Every mis-shape reads as ABSENT rather than as a violation: this feeds a
40265
+ * REFUSAL, so guessing at a container cdkd could not read would block a deploy
40266
+ * over a value it never understood. A genuinely broken `Policies` blob is AWS's
40267
+ * to reject, with its own message.
40268
+ *
40269
+ * The two containers are traversed with `?.` rather than with the
40270
+ * `typeof x === 'object' && !Array.isArray(x)` guard `config-shape.ts` uses,
40271
+ * and that is deliberate rather than sloppy: those guards are load-bearing
40272
+ * THERE because the helper REFUSES a non-object, while here the only outcome is
40273
+ * "no factors", which is exactly what indexing a string / number / array
40274
+ * already yields. The version carrying them was written first and MEASURED
40275
+ * inert -- deleting either clause failed no test and could not be made to,
40276
+ * since no template value indexes to anything at those keys. `?.` keeps the one
40277
+ * behaviour that is real (a declared `null` throws on a bare index) and leaves
40278
+ * nothing unfalsifiable behind.
40279
+ *
40280
+ * The two filters below are NOT the same kind of thing, and saying so matters
40281
+ * because an earlier revision of this comment claimed both were load-bearing and
40282
+ * "each fenced by a test" -- which was false for one of them, and a false fence
40283
+ * claim is what stops the next reader re-checking.
40284
+ *
40285
+ * - `Array.isArray` is a RUNTIME guard and IS fenced: without it a scalar
40286
+ * `AllowedFirstAuthFactors: 'EMAIL_OTP'` is read as a declaration and
40287
+ * refused, and the test for that shape reds when it is dropped.
40288
+ * - The string filter is a TYPE NARROWING and is INERT at runtime. `includes`
40289
+ * compares by SameValueZero, so a non-string member can never equal a
40290
+ * deny-list entry however it is spelled; deleting the filter fails NO test,
40291
+ * and no test could be written that it would fail. It earns its place by
40292
+ * giving the caller a `string[]`, and by keeping this function honest if a
40293
+ * future caller ever COERCES a member instead of comparing it. The
40294
+ * nested-list case in the suite fences that hypothetical `String(member)`
40295
+ * implementation -- i.e. the mistake, not this code.
40296
+ */
40297
+ function readAllowedFirstAuthFactors(properties) {
40298
+ const factors = (properties["Policies"]?.["SignInPolicy"])?.["AllowedFirstAuthFactors"];
40299
+ if (!Array.isArray(factors)) return [];
40300
+ return factors.filter((factor) => typeof factor === "string");
40301
+ }
40302
+ /**
40303
+ * The PRE-FLIGHT: the reason this user-pool configuration cannot be applied at
40304
+ * all, or `undefined` when nothing is known to refuse it.
40305
+ *
40306
+ * Two rules, both for combinations AWS rejects 100% of the time, and both
40307
+ * raised BEFORE the first AWS call on BOTH the create and the update path. The
40308
+ * ordering is what makes them worth refusing rather than warning about: on the
40309
+ * UPDATE path `UpdateUserPool` lands FIRST and `SetUserPoolMfaConfig` is
40310
+ * refused afterwards, so sending the request leaves a PARTIAL APPLY behind --
40311
+ * with the security-relevant half being the half that did NOT apply (the MFA
40312
+ * configuration), while the half that did is the one loosening authentication.
40313
+ * Nothing unwinds it, and a retry cannot succeed until the user reverse-engineers
40314
+ * an AWS-worded error about a field they did not think they were changing.
40315
+ *
40316
+ * Rule 1 (issue #1977) -- `MfaConfiguration` resolves to OFF while the request
40317
+ * carries an MFA factor block. MEASURED us-east-1 2026-08-19 (issue #1968):
40318
+ *
40319
+ * {ON, SoftwareTokenMfaConfiguration: {Enabled: true}} -> ACCEPTED (control)
40320
+ * {OFF, SoftwareTokenMfaConfiguration: {Enabled: true}} -> rejected
40321
+ * {OFF, SoftwareTokenMfaConfiguration: {Enabled: false}} -> rejected
40322
+ * {OFF, EmailMfaConfiguration: {Message, Subject}} -> rejected
40323
+ *
40324
+ * all with `InvalidParameterException: Invalid MFA configuration given, can't
40325
+ * turn off MFA and configure an MFA together`. The `Enabled: false` arm settles
40326
+ * the KEY: a block that enables NOTHING is still refused, so the rule is about
40327
+ * the block's PRESENCE -- exactly what `hasAnyMfaFactorBlock` tests -- and
40328
+ * since that predicate is precisely the three factor sub-blocks, no accepted
40329
+ * case survives. (Bound on the Email arm: an OPTIONAL control with that block
40330
+ * failed with a DIFFERENT error, "Cannot set EmailMfaConfiguration when user
40331
+ * pool EmailConfiguration contains an EmailSendingAccount of COGNITO_DEFAULT",
40332
+ * so it has no isolated control of its own -- informative in itself, since it
40333
+ * shows the exclusion is evaluated BEFORE the email-sending-account check.)
40334
+ *
40335
+ * Rule 2 (issue #1975) -- the `SetUserPoolMfaConfig` request resolves
40336
+ * `MfaConfiguration` to ON while `Policies.SignInPolicy.AllowedFirstAuthFactors`
40337
+ * allows `EMAIL_OTP` or `SMS_OTP`. AWS refuses with `Only PASSWORD and WEB_AUTHN
40338
+ * (if configured) can be enabled as an auth factor if MFA is enabled`. MEASURED
40339
+ * us-east-1 2026-08-19, both members and both MFA modes:
40340
+ *
40341
+ * {[PASSWORD, EMAIL_OTP], ON} -> rejected
40342
+ * {[PASSWORD, EMAIL_OTP], OPTIONAL} -> ACCEPTED
40343
+ * {[PASSWORD, SMS_OTP], ON} -> rejected (pool WITH a valid SmsConfiguration)
40344
+ * {[PASSWORD, SMS_OTP], OPTIONAL} -> ACCEPTED
40345
+ *
40346
+ * So `=== 'ON'` is MEASURED-CORRECT rather than merely conservative: OPTIONAL is
40347
+ * ACCEPTED, and widening this to `!== 'OFF'` would REFUSE A WORKING DEPLOY. An
40348
+ * earlier revision of this comment called OPTIONAL unmeasured; it is not, and
40349
+ * the measurement is what settles the narrowing rather than a cost argument.
40350
+ *
40351
+ * WHY THE MESSAGE DOES NOT OFFER `WEB_AUTHN` AS THE ESCAPE HATCH, even though
40352
+ * AWS's own error text names it as accepted. MEASURED us-east-1 2026-08-20: a
40353
+ * pool allowing `WEB_AUTHN` as a first auth factor is REJECTED by
40354
+ * `SetUserPoolMfaConfig(ON, SoftwareTokenMfa)` --
40355
+ *
40356
+ * InvalidParameterException: Cannot set WebAuthn factor configuration to
40357
+ * SINGLE_FACTOR if MFA is required and WebAuthn is an allowed first auth factor
40358
+ *
40359
+ * -- with NO WebAuthn block at all, with `UserVerification: preferred`, and with
40360
+ * `required`. The only accepted shape is
40361
+ * `WebAuthnConfiguration.FactorConfiguration = MULTI_FACTOR_WITH_USER_VERIFICATION`,
40362
+ * and the PINNED SDK (`@aws-sdk/client-cognito-identity-provider` 3.1018.0)
40363
+ * does not carry that field -- `WebAuthnConfigurationType` is exactly
40364
+ * `{RelyingPartyId?, UserVerification?}` -- so cdkd cannot reach it today.
40365
+ * Telling the user to switch to `WEB_AUTHN` would therefore be advice that
40366
+ * cannot be followed, which is why the remedy names removing the factor or
40367
+ * lowering `MfaConfiguration` instead.
40368
+ *
40369
+ * That same measurement means `WEB_AUTHN` + ON reproduces this very
40370
+ * partial-apply shape while NOT being on the deny-list. Deliberately left
40371
+ * uncovered: the correct rule there is CONDITIONAL on
40372
+ * `WebAuthnConfiguration.FactorConfiguration`, which the pinned SDK cannot
40373
+ * express, so a flat deny-list entry would be an OVER-REFUSAL of the shape that
40374
+ * becomes valid the moment the SDK gains the field. Tracked as issue #2064.
40375
+ *
40376
+ * SCOPE LIMIT, deliberate and load-bearing: rule 2 fires only when a
40377
+ * `SetUserPoolMfaConfig` request is actually BUILT (`hasMfaConfigProps`), i.e.
40378
+ * the template declares `EnabledMfas` / an email-OTP message or subject /
40379
+ * WebAuthn. With NONE of those, `MfaConfiguration` rides on the SINGLE
40380
+ * `CreateUserPool` / `UpdateUserPool` call and no second call follows -- so
40381
+ * there is no two-call window and structurally NOTHING to partly apply, which
40382
+ * is the entire harm this pre-flight exists to prevent. Refusing there would
40383
+ * ALSO be an over-refusal on a shape AWS accepts: a pool created with
40384
+ * `AllowedFirstAuthFactors: [PASSWORD, EMAIL_OTP]` and no MFA configuration
40385
+ * call at all is ACCEPTED (measured, same session -- the pool came up fine and
40386
+ * only the later `SetUserPoolMfaConfig(ON)` failed). Such a template is left to
40387
+ * AWS, which answers it atomically.
40388
+ *
40389
+ * CLOUDFORMATION PARITY, settled by A/B rather than assumed -- both issues made
40390
+ * this an explicit precondition. CloudFormation hits the SAME rejections and
40391
+ * rolls the stack back:
40392
+ *
40393
+ * {MfaConfiguration: OFF, EnabledMfas: [SOFTWARE_TOKEN_MFA]}
40394
+ * -> ROLLBACK_COMPLETE, "Invalid MFA configuration given, can't turn off
40395
+ * MFA and configure an MFA together"
40396
+ * {MfaConfiguration: ON, EnabledMfas: [SOFTWARE_TOKEN_MFA],
40397
+ * AllowedFirstAuthFactors: [PASSWORD, EMAIL_OTP]} (ESSENTIALS tier)
40398
+ * -> ROLLBACK_COMPLETE, "Only PASSWORD and WEB_AUTHN..."
40399
+ *
40400
+ * So refusing is PARITY-PRESERVING, not a divergence: cdkd refuses the same SET
40401
+ * of templates CloudFormation refuses, only EARLIER (before the first API call
40402
+ * instead of after a partial apply) and in cdkd's own wording. Nothing that
40403
+ * deploys under CloudFormation is blocked here.
40404
+ *
40405
+ * Both rules read the value cdkd would SEND rather than the raw template value,
40406
+ * from ONE `buildMfaConfigRequest` call, so the refusal and the request can
40407
+ * never disagree about what OFF / ON means for a given bag. The build runs with
40408
+ * NO logger, so the pre-flight is silent and `applyMfaConfig`'s own warnings
40409
+ * still fire exactly once.
40410
+ *
40411
+ * WHAT THIS CANNOT SEE, stated because the message says "refuses it before
40412
+ * sending anything" and that reads as a completeness claim: the pre-flight
40413
+ * inspects the TEMPLATE, never the live pool, so two template edits reach the
40414
+ * same partial apply without tripping it -- DELETING the `SignInPolicy` block
40415
+ * while setting `MfaConfiguration: ON`, and OMITTING `MfaConfiguration` while
40416
+ * adding `EMAIL_OTP` to a pool whose live MFA is already ON. Both need a
40417
+ * live-state guard and are tracked in #2051.
40418
+ *
40419
+ * Deliberately UNCONDITIONAL -- no `CreateContext.replayingState` downgrade.
40420
+ * `.claude/rules/providers.md` says a create-path pre-flight refusal MUST
40421
+ * downgrade on a replay, so this is a STATED EXCEPTION rather than an
40422
+ * oversight, and the rule's own reasoning is what licenses it: the downgrade
40423
+ * exists because a refusal against a STATE record leaves a resource
40424
+ * un-rollbackable with no template-side remedy. That presupposes the replay
40425
+ * could otherwise SUCCEED. Here it cannot -- both combinations are rejected by
40426
+ * AWS 100% of the time (measured above, and CloudFormation rolls back on the
40427
+ * same templates), so downgrading would trade a clear cdkd-worded refusal for
40428
+ * the identical failure arriving later from AWS, on the create path with a
40429
+ * pool to roll back and on the update path with a partial apply. A state record
40430
+ * additionally cannot legitimately HOLD either combination: state is written
40431
+ * only after a SUCCESSFUL apply, and neither can be applied. Revisit this if
40432
+ * AWS ever starts accepting one of them.
40433
+ */
40434
+ function describeUnsupportedMfaCombination(properties, declaredMfaConfiguration) {
40435
+ const request = buildMfaConfigRequest("", properties, void 0, declaredMfaConfiguration);
40436
+ if (request && request.MfaConfiguration === "OFF" && hasAnyMfaFactorBlock(request)) {
40437
+ const blocks = [
40438
+ ...request.SmsMfaConfiguration !== void 0 ? ["SmsMfaConfiguration"] : [],
40439
+ ...request.SoftwareTokenMfaConfiguration !== void 0 ? ["SoftwareTokenMfaConfiguration"] : [],
40440
+ ...request.EmailMfaConfiguration !== void 0 ? ["EmailMfaConfiguration"] : []
40441
+ ];
40442
+ return `${MFA_CONFIGURATION_PATH} is OFF while an MFA factor is configured (SetUserPoolMfaConfig would carry ${blocks.join(", ")}, built from EnabledMfas / EmailAuthenticationMessage / EmailAuthenticationSubject). AWS rejects that combination ("Invalid MFA configuration given, can't turn off MFA and configure an MFA together"), so cdkd refuses it before sending anything rather than leaving a partly-applied update behind. Set MfaConfiguration to ON or OPTIONAL (or remove it) to enable the declared factor. (If MFA is genuinely not wanted, remove EnabledMfas / EmailAuthenticationMessage / EmailAuthenticationSubject instead.)`;
40443
+ }
40444
+ if (request && request.MfaConfiguration === "ON") {
40445
+ const denied = MFA_INCOMPATIBLE_FIRST_AUTH_FACTORS;
40446
+ const offending = readAllowedFirstAuthFactors(properties).filter((factor) => denied.includes(factor));
40447
+ if (offending.length > 0) return `${MFA_CONFIGURATION_PATH} is ON while Policies.SignInPolicy.AllowedFirstAuthFactors allows ${offending.join(", ")}. AWS rejects that combination ("Only PASSWORD and WEB_AUTHN (if configured) can be enabled as an auth factor if MFA is enabled"), and on an update the sign-in policy has already been applied by the time it does, so cdkd refuses it before sending anything rather than leaving a partly-applied update behind. Remove ${offending.join(", ")} from Policies.SignInPolicy.AllowedFirstAuthFactors, leaving PASSWORD. (Setting MfaConfiguration to OPTIONAL or OFF also clears the conflict, but weakens MFA.)`;
40448
+ }
40449
+ }
40450
+ /**
40078
40451
  * Replace a DECLARED-but-unusable `MfaConfiguration` with the value cdkd
40079
40452
  * actually sends, or drop the key when nothing is sent. Returns the input
40080
40453
  * object unchanged when nothing applies, so the common case compares
@@ -40158,7 +40531,7 @@ var CognitoUserPoolProvider = class {
40158
40531
  "WebAuthnRelyingPartyID",
40159
40532
  "WebAuthnUserVerification"
40160
40533
  ])]]);
40161
- unhandledByDesign = /* @__PURE__ */ new Map([["AWS::Cognito::UserPool", /* @__PURE__ */ new Map([["WebAuthnFactorConfiguration", "No SDK wire path: @aws-sdk/client-cognito-identity-provider has no field accepting SINGLE_FACTOR | MULTI_FACTOR_WITH_USER_VERIFICATION (not on CreateUserPool/UpdateUserPool, nor SetUserPoolMfaConfig.WebAuthnConfiguration which only carries RelyingPartyId/UserVerification); CC-API-registry-only property"]])]]);
40534
+ unhandledByDesign = /* @__PURE__ */ new Map([["AWS::Cognito::UserPool", /* @__PURE__ */ new Map([["WebAuthnFactorConfiguration", "No wire path in the PINNED SDK (@aws-sdk/client-cognito-identity-provider 3.1018.0): WebAuthnConfigurationType is {RelyingPartyId?, UserVerification?} and no CreateUserPool/UpdateUserPool field accepts SINGLE_FACTOR | MULTI_FACTOR_WITH_USER_VERIFICATION. This is an SDK-VERSION limit, NOT an API one -- the live API does honour FactorConfiguration (measured us-east-1 2026-08-20: SetUserPoolMfaConfig(ON) on a pool allowing WEB_AUTHN as a first auth factor is rejected with \"Cannot set WebAuthn factor configuration to SINGLE_FACTOR if MFA is required and WebAuthn is an allowed first auth factor\", i.e. the service reads a field the SDK cannot send). RE-EVALUATE THIS ENTRY ON AN SDK BUMP: if WebAuthnConfigurationType gains FactorConfiguration, the property becomes handleable and this entry must go"]])]]);
40162
40535
  /**
40163
40536
  * Warn when `MfaConfiguration` is declared as a BLANK string (issue #1925
40164
40537
  * item 2, review round 2).
@@ -40187,6 +40560,26 @@ var CognitoUserPoolProvider = class {
40187
40560
  this.logger.warn("AWS::Cognito::UserPool MfaConfiguration is an empty string, which declares no MFA mode and is treated as absent -- a collapsed Fn::Join or an empty String parameter produces this shape. The pool deploys with the default MFA configuration instead; declare ON / OPTIONAL / OFF explicitly, or omit the property, to say which was meant.");
40188
40561
  }
40189
40562
  /**
40563
+ * Raise {@link describeUnsupportedMfaCombination}'s verdict as a typed
40564
+ * refusal, or return silently when there is nothing to refuse.
40565
+ *
40566
+ * A `ProvisioningError` so both call sites behave the way each already does
40567
+ * with the sibling `MfaConfiguration` shape refusal: on the create path it
40568
+ * surfaces as the provider's own error rather than an AWS one, and on the
40569
+ * update path `update()`'s catch re-throws an already-wrapped
40570
+ * `ProvisioningError` unchanged, so it is not double-wrapped. Both call sites
40571
+ * are OUTSIDE their method's try, so the throw travels untouched either way.
40572
+ *
40573
+ * `target` is the pool NAME on create (there is no physical id yet) and the
40574
+ * physical id on update, matching what each path passes to its other
40575
+ * `ProvisioningError`s.
40576
+ */
40577
+ assertMfaCombinationApplicable(logicalId, resourceType, target, properties, declaredMfaConfiguration) {
40578
+ const reason = describeUnsupportedMfaCombination(properties, declaredMfaConfiguration);
40579
+ if (reason === void 0) return;
40580
+ throw new ProvisioningError(reason, resourceType, logicalId, target);
40581
+ }
40582
+ /**
40190
40583
  * The DIFF-side half of the `MfaConfiguration` substitution record.
40191
40584
  *
40192
40585
  * Applied by `DiffCalculator` to BOTH comparison sides, so a template that
@@ -40276,6 +40669,7 @@ var CognitoUserPoolProvider = class {
40276
40669
  } catch (error) {
40277
40670
  throw new ProvisioningError(error instanceof Error ? error.message : String(error), resourceType, logicalId, poolName, error instanceof Error ? error : void 0);
40278
40671
  }
40672
+ this.assertMfaCombinationApplicable(logicalId, resourceType, poolName, properties, mfaConfiguration);
40279
40673
  let createdUserPoolId;
40280
40674
  try {
40281
40675
  const createParams = { PoolName: poolName };
@@ -40445,7 +40839,8 @@ var CognitoUserPoolProvider = class {
40445
40839
  * sub-block in the request AWS REJECTS the call; with none it accepts and
40446
40840
  * resets the pool to MFA-disabled. An EXPLICIT OFF beside a block is
40447
40841
  * rejected the same way (2026-08-19, issue #1968; the transcript lives at
40448
- * the pinned-OFF warning in `buildMfaConfigRequest`).
40842
+ * `describeUnsupportedMfaCombination`, which since issue #1977 REFUSES that
40843
+ * combination before any call rather than warning about it).
40449
40844
  *
40450
40845
  * Nothing here licenses a claim about a field on neither list, or about one
40451
40846
  * API from the other's section. Measure it.
@@ -40523,6 +40918,7 @@ var CognitoUserPoolProvider = class {
40523
40918
  this.logger.debug(`Updating Cognito User Pool ${logicalId}: ${physicalId}`);
40524
40919
  const mfaConfiguration = readDeclaredMfaConfiguration(properties["MfaConfiguration"], { onUnusable: (message) => this.logger.warn(message) });
40525
40920
  this.warnOnBlankMfaConfiguration(properties["MfaConfiguration"]);
40921
+ this.assertMfaCombinationApplicable(logicalId, resourceType, physicalId, properties, mfaConfiguration);
40526
40922
  const liveMfa = mfaConfiguration === "" && hasMfaConfigProps(properties) ? await this.readLiveMfaConfiguration(physicalId) : void 0;
40527
40923
  try {
40528
40924
  const updateParams = { UserPoolId: physicalId };
@@ -82898,7 +83294,7 @@ function createMigrateCommand() {
82898
83294
  */
82899
83295
  function buildProgram() {
82900
83296
  const program = new Command();
82901
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.13");
83297
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.15");
82902
83298
  program.hook("preAction", (_thisCommand, actionCommand) => {
82903
83299
  const { profile } = actionCommand.optsWithGlobals();
82904
83300
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;