@go-to-k/cdkd 0.278.18 → 0.278.20

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 DagBuilder, A as red, At as resolveApp, B as isTerminationProtectionPropagationError, Bt as CFN_TEMPLATE_URL_LIMIT, C as isStatefulRecreateTargetSync, Ct as runDockerStreaming, D as cyan, Dt as synthesisStatusMessage, E as bold, Et as Synthesizer, F as ProviderRegistry, Ft as resolveStateBucketWithDefaultAndSource, G as normalizeAwsTagsToCfn, Gt as AssemblyReader, H as cfnRefValueFromPhysicalId, Ht as findLargeInlineResources, I as findActionableSilentDrops, It as resolveUseCdkBootstrapAssets, J as applyRoleArnIfSet, Jt as resolveBucketRegion, K as resolveExplicitPhysicalId, Kt as processStackMessages, L as CloudControlProvider, Lt as stateBucketExistenceConfirmed, M as IAMRoleProvider, Mt as resolveCaptureObservedState, N as collectInlinePolicyNamesManagedBySiblings, Nt as resolveSkipPrefix, O as gray, Ot as getDefaultStateBucketName, P as clearOnUpdateRemoval, Pt as resolveStateBucketWithDefault, Q as isRetryableTransientError, Qt as setAwsClients, R as slowCcOperationTimeoutMs, Rt as warnDeprecatedNoPrefixCliFlag, S as MULTI_REGION_RECREATE_BLOCKED_TYPES, St as runDockerForeground, T as formatResourceLine, Tt as getDockerImageBySourceHash, U as refStateLookupFromResource, Ut as uploadCfnTemplate, V as IntrinsicFunctionResolver, Vt as MIGRATE_TMP_PREFIX, W as WAFv2WebACLProvider, Wt as expectedOwnerParam, X as describeTypeWithThrottleRetry, Xt as getAwsClients, Y as DiffCalculator, Yt as AwsClients, Z as withRetry, Zt as resetAwsClients, _ as createPreDeleteFinalSnapshot, _t as validateAssetBucketName, a as DeploymentEventsStore, an as LocalMigrateError, at as AssetPublisher, b as unsupportedFinalSnapshotError, bn as normalizeAwsError, bt as formatDockerLoginError, c as replayFailedOperations, cn as MissingCdkCliError, ct as buildAssetRedirectMap, d as IMPLICIT_DELETE_DEPENDENCIES, dn as ProvisioningError, dt as rewriteTemplateAssetReferences, en as CdkdError, et as TemplateParser, f as computeImplicitDeleteEdges, fn as ResourceTimeoutError, ft as AssetModeResolver, g as ccRoutedFinalSnapshotError, gt as parseBootstrapMarker, h as buildFinalSnapshotIdentifier, hn as StackTerminationProtectionError, ht as getBootstrapMarkerKey, i as DeploymentEventsReader, in as LocalInvokeBuildError$1, it as shouldRetainResource, j as yellow, jt as resolveAutoAssetStorage, k as green, kt as getLegacyStateBucketName, l as replayRollback, ln as NestedStackChildDirectDestroyError, lt as createAssetRedirectResolver, m as PRE_DELETE_SNAPSHOT_TYPES, mn as StackHasActiveImportsError, mt as ensureAssetStorage, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as S3StateBackend, o as planFailedOps, on as LocalStartServiceError, ot as stringifyValue, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as ResourceUpdateNotSupportedError, pt as BOOTSTRAP_MARKER_PREFIX, q as assertRegionMatch, r as DeployEngine, rn as DeployCancelledError, rt as rebuildClientForBucketRegion, s as planRollback, st as WorkGraph, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as LockManager, u as withResourceDeadline, un as PartialFailureError, ut as loadPublishableAssetManifest, v as isFinalSnapshotError, vt as validateContainerRepoName, w as renderStatefulReason, wt as AssetManifestLoader, x as extractDeploymentEventError, xn as withErrorHandling, xt as getDockerCmd, y as refusesFinalSnapshot, yt as buildDockerImage, z as disableInstanceApiTermination, zt as CFN_TEMPLATE_BODY_LIMIT } from "./deploy-engine-D9TPJSZY.js";
2
+ import { $ as isThrottlingError, $t as setAwsClients, A as red, At as getLegacyStateBucketName, B as isTerminationProtectionPropagationError, Bt as CFN_TEMPLATE_BODY_LIMIT, C as isStatefulRecreateTargetSync, Ct as runDockerForeground, D as cyan, Dt as Synthesizer, E as bold, Et as getDockerImageBySourceHash, F as ProviderRegistry, Ft as resolveStateBucketWithDefault, G as normalizeAwsTagsToCfn, Gt as expectedOwnerParam, H as cfnRefValueFromPhysicalId, Ht as MIGRATE_TMP_PREFIX, I as findActionableSilentDrops, It as resolveStateBucketWithDefaultAndSource, J as applyRoleArnIfSet, K as resolveExplicitPhysicalId, Kt as AssemblyReader, L as CloudControlProvider, Lt as resolveUseCdkBootstrapAssets, M as IAMRoleProvider, Mt as resolveAutoAssetStorage, N as collectInlinePolicyNamesManagedBySiblings, Nt as resolveCaptureObservedState, O as gray, Ot as synthesisStatusMessage, P as clearOnUpdateRemoval, Pt as resolveSkipPrefix, Q as isRetryableTransientError, Qt as resetAwsClients, R as slowCcOperationTimeoutMs, Rt as stateBucketExistenceConfirmed, S as MULTI_REGION_RECREATE_BLOCKED_TYPES, Sn as withErrorHandling, St as getDockerCmd, T as formatResourceLine, Tt as AssetManifestLoader, U as refStateLookupFromResource, Ut as findLargeInlineResources, V as IntrinsicFunctionResolver, Vt as CFN_TEMPLATE_URL_LIMIT, W as WAFv2WebACLProvider, Wt as uploadCfnTemplate, X as describeTypeWithThrottleRetry, Xt as AwsClients, Y as DiffCalculator, Yt as resolveBucketRegion, Z as withRetry, Zt as getAwsClients, _ as createPreDeleteFinalSnapshot, _t as parseBootstrapMarker, a as DeploymentEventsStore, an as LocalInvokeBuildError$1, at as shouldRetainResource, b as unsupportedFinalSnapshotError, bt as buildDockerImage, c as replayFailedOperations, ct as WorkGraph, d as IMPLICIT_DELETE_DEPENDENCIES, dn as PartialFailureError, dt as loadPublishableAssetManifest, et as DagBuilder, f as computeImplicitDeleteEdges, fn as ProvisioningError, ft as rewriteTemplateAssetReferences, g as ccRoutedFinalSnapshotError, gn as StackTerminationProtectionError, gt as getBootstrapMarkerKey, h as buildFinalSnapshotIdentifier, hn as StackHasActiveImportsError, ht as ensureAssetStorage, i as DeploymentEventsReader, in as DeployCancelledError, it as rebuildClientForBucketRegion, j as yellow, jt as resolveApp, k as green, kt as getDefaultStateBucketName, l as replayRollback, ln as MissingCdkCliError, lt as buildAssetRedirectMap, m as PRE_DELETE_SNAPSHOT_TYPES, mn as ResourceUpdateNotSupportedError, mt as BOOTSTRAP_MARKER_PREFIX, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as LockManager, o as planFailedOps, on as LocalMigrateError, ot as AssetPublisher, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as ResourceTimeoutError, pt as AssetModeResolver, q as assertRegionMatch, qt as processStackMessages, r as DeployEngine, rt as S3StateBackend, s as planRollback, sn as LocalStartServiceError, st as stringifyValue, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as CdkdError, tt as TemplateParser, u as withResourceDeadline, un as NestedStackChildDirectDestroyError, ut as createAssetRedirectResolver, v as isFinalSnapshotError, vt as validateAssetBucketName, w as renderStatefulReason, wt as runDockerStreaming, x as extractDeploymentEventError, xn as normalizeAwsError, xt as formatDockerLoginError, y as refusesFinalSnapshot, yt as validateContainerRepoName, z as disableInstanceApiTermination, zt as warnDeprecatedNoPrefixCliFlag } from "./deploy-engine-Bv3IEbrU.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as withSkipPrefix, i as runStackBuffered, n as getLogger, o as PATTERN_B_NAME_OPTIONS, p as withStackName, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-BYMEE-BS.js";
4
- import { t as ASGProvider } from "./asg-provider-DHs3PmZU.js";
4
+ import { t as ASGProvider } from "./asg-provider-OB2CvUwn.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, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, 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";
@@ -12269,6 +12269,11 @@ var DynamoDBGlobalTableProvider = class {
12269
12269
  }
12270
12270
  }));
12271
12271
  }
12272
+ if (billingMode === "PROVISIONED") try {
12273
+ await this.reconcileAutoScalingTargets(tableName, [], collectAutoScalingTargets(properties, currentRegion), currentRegion);
12274
+ } catch (autoScalingErr) {
12275
+ 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.`);
12276
+ }
12272
12277
  this.logger.debug(`Successfully created DynamoDB GlobalTable ${logicalId}: ${tableName}`);
12273
12278
  return {
12274
12279
  physicalId: tableName,
@@ -12442,12 +12447,12 @@ var DynamoDBGlobalTableProvider = class {
12442
12447
  BillingMode: newBilling
12443
12448
  };
12444
12449
  if (newBilling === "PROVISIONED") {
12445
- billingUpdate.ProvisionedThroughput = derivePerCallProvisionedThroughput(properties, currentRegion);
12446
- const provisionedIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling);
12450
+ billingUpdate.ProvisionedThroughput = derivePerCallProvisionedThroughput(properties, currentRegion, "seed");
12451
+ const provisionedIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling, "seed");
12447
12452
  const previousCfnIndexes = previousProperties["GlobalSecondaryIndexes"];
12448
12453
  const existingIndexNames = new Set((Array.isArray(previousCfnIndexes) ? previousCfnIndexes : []).map((entry) => entry?.["IndexName"]).filter((name) => typeof name === "string"));
12449
12454
  const previousByIndexName = /* @__PURE__ */ new Map();
12450
- for (const gsi of toSdkGlobalSecondaryIndexes(previousProperties, currentRegion, newBilling)) if (gsi.IndexName) previousByIndexName.set(gsi.IndexName, gsi);
12455
+ for (const gsi of toSdkGlobalSecondaryIndexes(previousProperties, currentRegion, newBilling, "seed")) if (gsi.IndexName) previousByIndexName.set(gsi.IndexName, gsi);
12451
12456
  const byIndexName = new Map(previousByIndexName);
12452
12457
  const survivingIndexNames = /* @__PURE__ */ new Set();
12453
12458
  for (const gsi of provisionedIndexes) {
@@ -12476,7 +12481,14 @@ var DynamoDBGlobalTableProvider = class {
12476
12481
  const writeAutoScalingOld = (previousProperties["WriteProvisionedThroughputSettings"] ?? {})["WriteCapacityAutoScalingSettings"];
12477
12482
  const writeAutoScalingNew = (properties["WriteProvisionedThroughputSettings"] ?? {})["WriteCapacityAutoScalingSettings"];
12478
12483
  const effectiveNewAutoScaling = oldBilling === "PROVISIONED" && newBilling === "PAY_PER_REQUEST" ? void 0 : writeAutoScalingNew;
12479
- if ((newBilling === "PROVISIONED" || oldBilling === "PROVISIONED") && !deepEqual$1(writeAutoScalingOld, effectiveNewAutoScaling)) await this.applyAutoScalingDiff(physicalId, "dynamodb:table:WriteCapacityUnits", writeAutoScalingOld, effectiveNewAutoScaling);
12484
+ const autoScalingApplied = /* @__PURE__ */ new Set();
12485
+ if ((newBilling === "PROVISIONED" || oldBilling === "PROVISIONED") && !deepEqual$1(writeAutoScalingOld, effectiveNewAutoScaling)) {
12486
+ await this.applyAutoScalingDiff(physicalId, "dynamodb:table:WriteCapacityUnits", writeAutoScalingOld, effectiveNewAutoScaling);
12487
+ autoScalingApplied.add(autoScalingTargetKey({
12488
+ dimension: "dynamodb:table:WriteCapacityUnits",
12489
+ region: currentRegion
12490
+ }));
12491
+ }
12480
12492
  const replicaDiff = diffReplicas(previousProperties["Replicas"] ?? [], properties["Replicas"] ?? []);
12481
12493
  for (const replica of replicaDiff.removed) {
12482
12494
  const region = replica["Region"];
@@ -12485,6 +12497,10 @@ var DynamoDBGlobalTableProvider = class {
12485
12497
  if (removedReadAutoScaling) {
12486
12498
  const regionalAutoScalingClient = this.getRegionalAutoScalingClient(region);
12487
12499
  await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", removedReadAutoScaling, void 0, regionalAutoScalingClient);
12500
+ autoScalingApplied.add(autoScalingTargetKey({
12501
+ dimension: "dynamodb:table:ReadCapacityUnits",
12502
+ region
12503
+ }));
12488
12504
  }
12489
12505
  await this.dynamoDBClient.send(new UpdateTableCommand({
12490
12506
  TableName: physicalId,
@@ -12502,6 +12518,10 @@ var DynamoDBGlobalTableProvider = class {
12502
12518
  if (newBilling === "PROVISIONED" && newReadAutoScaling) {
12503
12519
  const regionalAutoScalingClient = this.getRegionalAutoScalingClient(region);
12504
12520
  await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", void 0, newReadAutoScaling, regionalAutoScalingClient);
12521
+ autoScalingApplied.add(autoScalingTargetKey({
12522
+ dimension: "dynamodb:table:ReadCapacityUnits",
12523
+ region
12524
+ }));
12505
12525
  }
12506
12526
  }
12507
12527
  for (const replica of replicaDiff.modified) {
@@ -12517,6 +12537,10 @@ var DynamoDBGlobalTableProvider = class {
12517
12537
  if ((newBilling === "PROVISIONED" || oldBilling === "PROVISIONED") && !deepEqual$1(oldReadAutoScaling, effectiveNewReadAutoScaling)) {
12518
12538
  const regionalAutoScalingClient = this.getRegionalAutoScalingClient(region);
12519
12539
  await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", oldReadAutoScaling, effectiveNewReadAutoScaling, regionalAutoScalingClient);
12540
+ autoScalingApplied.add(autoScalingTargetKey({
12541
+ dimension: "dynamodb:table:ReadCapacityUnits",
12542
+ region
12543
+ }));
12520
12544
  }
12521
12545
  const updateAction = { RegionName: region };
12522
12546
  if (replica["KMSMasterKeyId"] !== void 0) updateAction.KMSMasterKeyId = replica["KMSMasterKeyId"];
@@ -12535,6 +12559,7 @@ var DynamoDBGlobalTableProvider = class {
12535
12559
  }
12536
12560
  const previousSdkIndexes = toSdkGlobalSecondaryIndexes(previousProperties, currentRegion, oldBilling);
12537
12561
  const gsiDiff = diffGlobalSecondaryIndexes(previousSdkIndexes, toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling));
12562
+ const rawOnDemandDeclarations = collectRawOnDemandDeclarations(properties, currentRegion);
12538
12563
  const previousSdkByName = /* @__PURE__ */ new Map();
12539
12564
  for (const prev of previousSdkIndexes) if (prev.IndexName) previousSdkByName.set(prev.IndexName, prev);
12540
12565
  for (const gsi of gsiDiff.removed) {
@@ -12564,22 +12589,27 @@ var DynamoDBGlobalTableProvider = class {
12564
12589
  await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
12565
12590
  }
12566
12591
  for (const gsi of gsiDiff.modified) {
12567
- if (!gsi.IndexName) continue;
12592
+ if (typeof gsi.IndexName !== "string" || gsi.IndexName.length === 0) continue;
12568
12593
  if (gsiHandledByBillingFlip.has(gsi.IndexName)) continue;
12569
12594
  const billingFlipped = oldBilling !== newBilling;
12570
12595
  const update = { IndexName: gsi.IndexName };
12571
12596
  const previousOnDemand = previousSdkByName.get(gsi.IndexName)?.OnDemandThroughput;
12597
+ const declared = rawOnDemandDeclarations.get(gsi.IndexName);
12572
12598
  const onDemand = { ...gsi.OnDemandThroughput };
12599
+ const unresolvedMembers = [];
12573
12600
  if (!billingFlipped && previousOnDemand) {
12574
- if (previousOnDemand.MaxReadRequestUnits !== void 0 && onDemand.MaxReadRequestUnits === void 0) onDemand.MaxReadRequestUnits = ON_DEMAND_LIMIT_RESET;
12575
- if (previousOnDemand.MaxWriteRequestUnits !== void 0 && onDemand.MaxWriteRequestUnits === void 0) onDemand.MaxWriteRequestUnits = ON_DEMAND_LIMIT_RESET;
12601
+ if (previousOnDemand.MaxReadRequestUnits !== void 0 && onDemand.MaxReadRequestUnits === void 0) if (declared?.readDeclared) unresolvedMembers.push("MaxReadRequestUnits");
12602
+ else onDemand.MaxReadRequestUnits = ON_DEMAND_LIMIT_RESET;
12603
+ if (previousOnDemand.MaxWriteRequestUnits !== void 0 && onDemand.MaxWriteRequestUnits === void 0) if (declared?.writeDeclared) unresolvedMembers.push("MaxWriteRequestUnits");
12604
+ else onDemand.MaxWriteRequestUnits = ON_DEMAND_LIMIT_RESET;
12576
12605
  }
12606
+ if (unresolvedMembers.length > 0) this.logger.warn(`GSI '${gsi.IndexName}' on ${physicalId}: the template declares ${unresolvedMembers.join(" / ")} but the value did not resolve to a number, so the on-demand limit was left UNCHANGED on AWS rather than applied or cleared. This is usually an unresolved intrinsic in the template; resolve it to a literal to change the ceiling, or remove the property entirely to clear it.`);
12577
12607
  if (Object.keys(onDemand).length > 0) update.OnDemandThroughput = onDemand;
12578
12608
  if (!billingFlipped && gsi.ProvisionedThroughput) update.ProvisionedThroughput = gsi.ProvisionedThroughput;
12579
12609
  const previousSdk = previousSdkByName.get(gsi.IndexName);
12580
12610
  if (gsi.WarmThroughput !== void 0 && !deepEqual$1(gsi.WarmThroughput, previousSdk?.WarmThroughput)) update.WarmThroughput = gsi.WarmThroughput;
12581
12611
  if (!update.ProvisionedThroughput && !update.OnDemandThroughput && !update.WarmThroughput) {
12582
- if (!billingFlipped) 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.`);
12612
+ if (!billingFlipped && unresolvedMembers.length === 0) 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.`);
12583
12613
  continue;
12584
12614
  }
12585
12615
  const gsiUpdate = { Update: update };
@@ -12589,6 +12619,10 @@ var DynamoDBGlobalTableProvider = class {
12589
12619
  }));
12590
12620
  await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
12591
12621
  }
12622
+ if (newBilling === "PROVISIONED" || oldBilling === "PROVISIONED") {
12623
+ if (gsiDiff.added.length > 0) await this.waitForIndexesActive(physicalId, logicalId);
12624
+ await this.reconcileAutoScalingTargets(physicalId, collectAutoScalingTargets(previousProperties, currentRegion), newBilling === "PAY_PER_REQUEST" ? [] : collectAutoScalingTargets(properties, currentRegion), currentRegion, autoScalingApplied);
12625
+ }
12592
12626
  if (!deepEqual$1(properties["TimeToLiveSpecification"], previousProperties["TimeToLiveSpecification"])) {
12593
12627
  const sendTtl = async (cmd) => {
12594
12628
  try {
@@ -12759,11 +12793,10 @@ var DynamoDBGlobalTableProvider = class {
12759
12793
  * create-only field with no corresponding `RegisterScalableTarget`
12760
12794
  * surface; `readAutoScalingSettings` also explicitly skips it.
12761
12795
  */
12762
- async applyAutoScalingDiff(tableName, dimension, oldSettings, newSettings, client) {
12763
- const isWrite = dimension === "dynamodb:table:WriteCapacityUnits";
12764
- const policyName = isWrite ? `DynamoDBWriteCapacityUtilization:table/${tableName}` : `DynamoDBReadCapacityUtilization:table/${tableName}`;
12765
- const metricType = isWrite ? "DynamoDBWriteCapacityUtilization" : "DynamoDBReadCapacityUtilization";
12766
- const resourceId = `table/${tableName}`;
12796
+ async applyAutoScalingDiff(tableName, dimension, oldSettings, newSettings, client, indexName) {
12797
+ const metricType = dimension.endsWith("WriteCapacityUnits") ? "DynamoDBWriteCapacityUtilization" : "DynamoDBReadCapacityUtilization";
12798
+ const resourceId = autoScalingResourceId(tableName, indexName);
12799
+ const policyName = `${metricType}:${resourceId}`;
12767
12800
  const asClient = client ?? await this.getLocalAutoScalingClient();
12768
12801
  const oldEnabled = oldSettings !== void 0 && oldSettings !== null;
12769
12802
  const newEnabled = newSettings !== void 0 && newSettings !== null;
@@ -12776,13 +12809,16 @@ var DynamoDBGlobalTableProvider = class {
12776
12809
  return;
12777
12810
  }
12778
12811
  try {
12779
- await asClient.send(new RegisterScalableTargetCommand({
12812
+ await withRetry(() => asClient.send(new RegisterScalableTargetCommand({
12780
12813
  ServiceNamespace: "dynamodb",
12781
12814
  ResourceId: resourceId,
12782
12815
  ScalableDimension: dimension,
12783
12816
  MinCapacity: minCapacity,
12784
12817
  MaxCapacity: maxCapacity
12785
- }));
12818
+ })), `${resourceId} (${dimension})`, {
12819
+ isRetryable: (_message, error) => isThrottlingError(error),
12820
+ ...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
12821
+ });
12786
12822
  } catch (err) {
12787
12823
  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}`);
12788
12824
  return;
@@ -12801,14 +12837,17 @@ var DynamoDBGlobalTableProvider = class {
12801
12837
  if (tttCfg["ScaleOutCooldown"] !== void 0) targetTrackingConfig["ScaleOutCooldown"] = Number(tttCfg["ScaleOutCooldown"]);
12802
12838
  if (tttCfg["DisableScaleIn"] !== void 0) targetTrackingConfig["DisableScaleIn"] = Boolean(tttCfg["DisableScaleIn"]);
12803
12839
  try {
12804
- await asClient.send(new PutScalingPolicyCommand({
12840
+ await withRetry(() => asClient.send(new PutScalingPolicyCommand({
12805
12841
  PolicyName: policyName,
12806
12842
  ServiceNamespace: "dynamodb",
12807
12843
  ResourceId: resourceId,
12808
12844
  ScalableDimension: dimension,
12809
12845
  PolicyType: "TargetTrackingScaling",
12810
12846
  TargetTrackingScalingPolicyConfiguration: targetTrackingConfig
12811
- }));
12847
+ })), `${policyName}`, {
12848
+ isRetryable: (_message, error) => isThrottlingError(error),
12849
+ ...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
12850
+ });
12812
12851
  this.logger.debug(`Upserted auto-scaling policy ${policyName} on ${tableName} (${dimension})`);
12813
12852
  } catch (err) {
12814
12853
  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`);
@@ -12843,6 +12882,130 @@ var DynamoDBGlobalTableProvider = class {
12843
12882
  }
12844
12883
  }
12845
12884
  /**
12885
+ * Resolve the application-autoscaling client that owns a given region's
12886
+ * targets. The local region goes through the cached client; every other
12887
+ * region gets the per-region cached client.
12888
+ */
12889
+ async autoScalingClientForRegion(region, localRegion) {
12890
+ return region === localRegion ? await this.getLocalAutoScalingClient() : this.getRegionalAutoScalingClient(region);
12891
+ }
12892
+ /**
12893
+ * Probe which of the given targets are ALREADY registered, so the
12894
+ * reconcile can skip the ones that are both present and unchanged
12895
+ * (Issue #1419).
12896
+ *
12897
+ * Per region: `DescribeScalableTargets` (batched + paginated) AND
12898
+ * `DescribeScalingPolicies`, returning the set of `autoScalingTargetKey`s
12899
+ * for which BOTH a target and a target-tracking policy exist.
12900
+ *
12901
+ * Both halves are required. A target whose `PutScalingPolicy` failed is
12902
+ * registered but scales nothing, and `applyAutoScalingDiff` swallows that
12903
+ * failure into a WARN — so probing the target alone would mark it present
12904
+ * and skip it on every later deploy, which is the same silent never-scales
12905
+ * gap this whole change exists to close, one level down.
12906
+ *
12907
+ * Best-effort: on ANY failure it returns `null`, which the caller reads as
12908
+ * "presence unknown" and falls back to upserting everything — an extra
12909
+ * idempotent call is always safer than skipping a registration that was
12910
+ * never made.
12911
+ */
12912
+ async probeRegisteredTargets(tableName, specs, localRegion) {
12913
+ const byRegion = /* @__PURE__ */ new Map();
12914
+ for (const spec of specs) {
12915
+ const list = byRegion.get(spec.region);
12916
+ if (list) list.push(spec);
12917
+ else byRegion.set(spec.region, [spec]);
12918
+ }
12919
+ const indexPrefix = `${autoScalingResourceId(tableName)}/index/`;
12920
+ const keyOf = (resourceId, dimension, region) => {
12921
+ return `${region}|${dimension}|${resourceId.startsWith(indexPrefix) ? resourceId.slice(indexPrefix.length) : ""}`;
12922
+ };
12923
+ const present = /* @__PURE__ */ new Set();
12924
+ for (const [region, regionSpecs] of byRegion) {
12925
+ const resourceIds = [...new Set(regionSpecs.map((s) => autoScalingResourceId(tableName, s.indexName)))];
12926
+ const client = await this.autoScalingClientForRegion(region, localRegion);
12927
+ const targets = /* @__PURE__ */ new Set();
12928
+ const policies = /* @__PURE__ */ new Set();
12929
+ for (let i = 0; i < resourceIds.length; i += DESCRIBE_SCALABLE_TARGETS_BATCH) {
12930
+ const batch = resourceIds.slice(i, i + DESCRIBE_SCALABLE_TARGETS_BATCH);
12931
+ try {
12932
+ let nextToken;
12933
+ do {
12934
+ const res = await client.send(new DescribeScalableTargetsCommand({
12935
+ ServiceNamespace: "dynamodb",
12936
+ ResourceIds: batch,
12937
+ ...nextToken ? { NextToken: nextToken } : {}
12938
+ }));
12939
+ for (const target of res.ScalableTargets ?? []) {
12940
+ if (!target.ResourceId || !target.ScalableDimension) continue;
12941
+ targets.add(keyOf(target.ResourceId, target.ScalableDimension, region));
12942
+ }
12943
+ nextToken = res.NextToken;
12944
+ } while (nextToken);
12945
+ let policyToken;
12946
+ do {
12947
+ const res = await client.send(new DescribeScalingPoliciesCommand({
12948
+ ServiceNamespace: "dynamodb",
12949
+ ...policyToken ? { NextToken: policyToken } : {}
12950
+ }));
12951
+ for (const policy of res.ScalingPolicies ?? []) {
12952
+ if (!policy.ResourceId || !policy.ScalableDimension) continue;
12953
+ if (policy.PolicyType !== "TargetTrackingScaling") continue;
12954
+ policies.add(keyOf(policy.ResourceId, policy.ScalableDimension, region));
12955
+ }
12956
+ policyToken = res.NextToken;
12957
+ } while (policyToken);
12958
+ } catch (err) {
12959
+ this.logger.debug(`Could not probe existing auto-scaling targets on ${tableName} in ${region}: ${err instanceof Error ? err.message : String(err)} — re-asserting every target instead.`);
12960
+ return null;
12961
+ }
12962
+ }
12963
+ for (const key of targets) if (policies.has(key)) present.add(key);
12964
+ }
12965
+ return present;
12966
+ }
12967
+ /**
12968
+ * Reconcile the application-autoscaling targets a CFn template declares
12969
+ * against the ones its previous version declared (Issue #1419).
12970
+ *
12971
+ * **A desired target is re-asserted even when the template did not
12972
+ * change, unless it is known to already exist.** A purely diff-gated
12973
+ * register would never backfill the targets that are the entire subject
12974
+ * of this issue: on a table deployed before this fix the settings are
12975
+ * byte-identical on both sides of every subsequent deploy, so a diff
12976
+ * check sees no change and leaves the dimension unregistered forever.
12977
+ *
12978
+ * The naive form of that — upsert everything, every deploy — costs
12979
+ * `2 x (1 + N_gsi x (1 + N_replica))` serial round trips on EVERY deploy
12980
+ * of an autoscaled table (a 20-GSI, 3-replica table would spend well over
12981
+ * a hundred sequential calls re-asserting a steady state). So presence is
12982
+ * probed first with one batched `DescribeScalableTargets` per region, and
12983
+ * a target that already exists AND whose settings are unchanged is
12984
+ * skipped. When the probe fails, presence is unknown and everything is
12985
+ * upserted — the correct direction to fail.
12986
+ */
12987
+ async reconcileAutoScalingTargets(tableName, previousSpecs, desiredSpecs, localRegion, alreadyApplied = /* @__PURE__ */ new Set()) {
12988
+ const previousSpecsToUse = previousSpecs.filter((s) => !alreadyApplied.has(autoScalingTargetKey(s)));
12989
+ const desiredSpecsToUse = desiredSpecs.filter((s) => !alreadyApplied.has(autoScalingTargetKey(s)));
12990
+ const previousByKey = /* @__PURE__ */ new Map();
12991
+ for (const spec of previousSpecsToUse) previousByKey.set(autoScalingTargetKey(spec), spec);
12992
+ const registered = desiredSpecsToUse.length > 0 && previousSpecsToUse.length > 0 ? await this.probeRegisteredTargets(tableName, desiredSpecsToUse, localRegion) : /* @__PURE__ */ new Set();
12993
+ const handled = /* @__PURE__ */ new Set();
12994
+ for (const spec of desiredSpecsToUse) {
12995
+ const key = autoScalingTargetKey(spec);
12996
+ handled.add(key);
12997
+ const previous = previousByKey.get(key);
12998
+ if (registered?.has(key) && previous !== void 0 && deepEqual$1(previous.settings, spec.settings)) continue;
12999
+ const client = await this.autoScalingClientForRegion(spec.region, localRegion);
13000
+ await this.applyAutoScalingDiff(tableName, spec.dimension, previous?.settings, spec.settings, client, spec.indexName);
13001
+ }
13002
+ for (const [key, spec] of previousByKey) {
13003
+ if (handled.has(key)) continue;
13004
+ const client = await this.autoScalingClientForRegion(spec.region, localRegion);
13005
+ await this.applyAutoScalingDiff(tableName, spec.dimension, spec.settings, void 0, client, spec.indexName);
13006
+ }
13007
+ }
13008
+ /**
12846
13009
  * Delete a DynamoDB Global Table.
12847
13010
  *
12848
13011
  * Order is load-bearing:
@@ -12883,12 +13046,15 @@ var DynamoDBGlobalTableProvider = class {
12883
13046
  throw new ProvisioningError(`Could not resolve client region for DynamoDB GlobalTable ${logicalId} delete — would risk dropping the local replica`, resourceType, logicalId, physicalId, cause);
12884
13047
  }
12885
13048
  try {
12886
- const replicas = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }))).Table?.Replicas ?? [];
13049
+ const describe = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }));
13050
+ const replicas = describe.Table?.Replicas ?? [];
13051
+ const tableIndexNames = (describe.Table?.GlobalSecondaryIndexes ?? []).map((gsi) => gsi.IndexName).filter((name) => typeof name === "string");
12887
13052
  for (const replica of replicas) {
12888
13053
  const region = replica.RegionName;
12889
13054
  if (!region || region === currentRegion) continue;
12890
13055
  try {
12891
13056
  await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", {}, void 0, this.getRegionalAutoScalingClient(region));
13057
+ for (const indexName of tableIndexNames) await this.applyAutoScalingDiff(physicalId, "dynamodb:index:ReadCapacityUnits", {}, void 0, this.getRegionalAutoScalingClient(region), indexName);
12892
13058
  await this.dynamoDBClient.send(new UpdateTableCommand({
12893
13059
  TableName: physicalId,
12894
13060
  ReplicaUpdates: [{ Delete: { RegionName: region } }]
@@ -12901,6 +13067,10 @@ var DynamoDBGlobalTableProvider = class {
12901
13067
  const localAsClient = await this.getLocalAutoScalingClient();
12902
13068
  await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", {}, void 0, localAsClient);
12903
13069
  await this.applyAutoScalingDiff(physicalId, "dynamodb:table:WriteCapacityUnits", {}, void 0, localAsClient);
13070
+ for (const indexName of tableIndexNames) {
13071
+ await this.applyAutoScalingDiff(physicalId, "dynamodb:index:WriteCapacityUnits", {}, void 0, localAsClient, indexName);
13072
+ await this.applyAutoScalingDiff(physicalId, "dynamodb:index:ReadCapacityUnits", {}, void 0, localAsClient, indexName);
13073
+ }
12904
13074
  } catch (describeErr) {
12905
13075
  if (!(describeErr instanceof ResourceNotFoundException$1)) {
12906
13076
  const cause = describeErr instanceof Error ? describeErr : void 0;
@@ -13284,6 +13454,34 @@ var DynamoDBGlobalTableProvider = class {
13284
13454
  throw new ProvisioningError(`Table ${tableName} did not reach ACTIVE within ${maxAttempts}s after UpdateTable`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
13285
13455
  }
13286
13456
  /**
13457
+ * Wait until every GSI reports `IndexStatus: ACTIVE` (Issue #1419).
13458
+ *
13459
+ * `waitForTableActiveAfterUpdate` only checks `TableStatus`, and a GSI
13460
+ * added by `UpdateTable` leaves the TABLE ACTIVE while the index itself
13461
+ * is still `CREATING` — application-autoscaling rejects a target whose
13462
+ * resource is not ready, so registering right after the GSI-add loop
13463
+ * would skip the very index the deploy just introduced.
13464
+ *
13465
+ * **Best-effort by design**: on timeout it warns and returns rather than
13466
+ * throwing. A missed registration self-heals on the next deploy (the
13467
+ * reconcile re-asserts anything not already present), whereas throwing
13468
+ * here would fail a deploy whose actual resources are all correct. Index
13469
+ * backfill on a large table can take a long time, hence the 15-minute cap.
13470
+ */
13471
+ async waitForIndexesActive(tableName, logicalId) {
13472
+ const maxAttempts = 900;
13473
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
13474
+ try {
13475
+ if (((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.GlobalSecondaryIndexes ?? []).every((gsi) => gsi.IndexStatus === "ACTIVE")) return;
13476
+ } catch (err) {
13477
+ this.logger.debug(`DescribeTable while waiting for indexes on ${tableName}: ${err instanceof Error ? err.message : String(err)}`);
13478
+ return;
13479
+ }
13480
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
13481
+ }
13482
+ this.logger.warn(`Indexes on ${tableName} (${logicalId}) did not all reach ACTIVE within ${maxAttempts}s; auto-scaling registration for a still-building index may have been skipped. The next deploy re-asserts it.`);
13483
+ }
13484
+ /**
13287
13485
  * Wait until a specific replica's `ReplicaStatus` flips to ACTIVE.
13288
13486
  * Replica provisioning typically takes 1–5 min; cap at 10 min.
13289
13487
  */
@@ -13339,26 +13537,6 @@ var DynamoDBGlobalTableProvider = class {
13339
13537
  * GlobalSecondaryIndexes, TableClassOverride — differ from the old shape).
13340
13538
  */
13341
13539
  /**
13342
- * Derive the per-call `ProvisionedThroughput` shape required by
13343
- * `CreateTableCommand` / `UpdateTableCommand` when BillingMode flips to
13344
- * PROVISIONED. Shared between create() and the BillingMode-flip path in
13345
- * update() so a user template's non-default read/write capacity is
13346
- * preserved consistently across both code paths.
13347
- *
13348
- * Source of truth (CFn `AWS::DynamoDB::GlobalTable` shape):
13349
- * - WriteCapacityUnits → `properties.WriteProvisionedThroughputSettings`
13350
- * (top-level on the table). That block's ONLY member is
13351
- * `WriteCapacityAutoScalingSettings` — write capacity on a GlobalTable is
13352
- * always auto-scaled, so there is no literal `WriteCapacityUnits` to
13353
- * prefer. `CreateTable` needs one concrete number, so the auto-scaling
13354
- * block's `SeedCapacity` (documented as the value used until the scaling
13355
- * policies exist) is taken before `MinCapacity`; 5 if the block is absent.
13356
- * - ReadCapacityUnits → `Replicas[?Region==<region>].ReadProvisionedThroughputSettings`
13357
- * (per-replica, the deploy region's setting). That block DOES carry a
13358
- * literal `ReadCapacityUnits`, which is taken before its auto-scaling
13359
- * `SeedCapacity` / `MinCapacity`; 5 if absent.
13360
- */
13361
- /**
13362
13540
  * Extract the local replica's `DeletionProtectionEnabled` from a CFn
13363
13541
  * properties shape. The `AWS::DynamoDB::GlobalTable` CFn schema places
13364
13542
  * the field inside `Replicas[?Region==<region>].DeletionProtectionEnabled`;
@@ -13378,10 +13556,30 @@ function extractLocalDeletionProtection(props, region) {
13378
13556
  const topLevel = props["DeletionProtectionEnabled"];
13379
13557
  return typeof topLevel === "boolean" ? topLevel : void 0;
13380
13558
  }
13381
- function derivePerCallProvisionedThroughput(properties, region) {
13559
+ /**
13560
+ * Derive the per-call `ProvisionedThroughput` shape required by
13561
+ * `CreateTableCommand` / `UpdateTableCommand` when BillingMode is
13562
+ * PROVISIONED. Shared between create() and the BillingMode-flip path in
13563
+ * update() so a user template's non-default read/write capacity is
13564
+ * preserved consistently across both code paths.
13565
+ *
13566
+ * Source of truth (CFn `AWS::DynamoDB::GlobalTable` shape):
13567
+ * - WriteCapacityUnits → `properties.WriteProvisionedThroughputSettings`
13568
+ * (top-level on the table). That block's ONLY member is
13569
+ * `WriteCapacityAutoScalingSettings` — write capacity on a GlobalTable is
13570
+ * always auto-scaled, so there is no literal `WriteCapacityUnits` to
13571
+ * prefer. `CreateTable` needs one concrete number; which member of the
13572
+ * auto-scaling block supplies it is context-dependent — see
13573
+ * {@link CapacitySource}, which `source` selects. 5 if the block is absent.
13574
+ * - ReadCapacityUnits → `Replicas[?Region==<region>].ReadProvisionedThroughputSettings`
13575
+ * (per-replica, the deploy region's setting). That block DOES carry a
13576
+ * literal `ReadCapacityUnits`, which is taken before its auto-scaling
13577
+ * members; 5 if absent.
13578
+ */
13579
+ function derivePerCallProvisionedThroughput(properties, region, source = "min") {
13382
13580
  return {
13383
- ReadCapacityUnits: deriveReadCapacityUnits(asRecord((properties["Replicas"] ?? []).find((r) => r["Region"] === region)?.["ReadProvisionedThroughputSettings"])) ?? DEFAULT_CAPACITY_UNITS,
13384
- WriteCapacityUnits: deriveWriteCapacityUnits(asRecord(properties["WriteProvisionedThroughputSettings"])) ?? DEFAULT_CAPACITY_UNITS
13581
+ ReadCapacityUnits: deriveReadCapacityUnits(asRecord((properties["Replicas"] ?? []).find((r) => r["Region"] === region)?.["ReadProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS,
13582
+ WriteCapacityUnits: deriveWriteCapacityUnits(asRecord(properties["WriteProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS
13385
13583
  };
13386
13584
  }
13387
13585
  /**
@@ -13389,6 +13587,110 @@ function derivePerCallProvisionedThroughput(properties, region) {
13389
13587
  * capacity value at all. Matches the pre-existing table-level default.
13390
13588
  */
13391
13589
  const DEFAULT_CAPACITY_UNITS = 5;
13590
+ /**
13591
+ * Stable identity for an {@link AutoScalingTargetSpec}, used to diff the
13592
+ * previous template's targets against the desired ones.
13593
+ */
13594
+ function autoScalingTargetKey(spec) {
13595
+ return `${spec.region}|${spec.dimension}|${spec.indexName ?? ""}`;
13596
+ }
13597
+ /**
13598
+ * The application-autoscaling `ResourceId` for a DynamoDB table or one of
13599
+ * its indexes. ONE spelling, shared by the register / policy / deregister
13600
+ * calls, the presence probe, and the target key — a divergence between any
13601
+ * two of those would silently register one target and look up another.
13602
+ */
13603
+ function autoScalingResourceId(tableName, indexName) {
13604
+ return indexName ? `table/${tableName}/index/${indexName}` : `table/${tableName}`;
13605
+ }
13606
+ /**
13607
+ * Test seam for the auto-scaling throttle retry's backoff, mirroring
13608
+ * `describeTypeRetryDelays`. Production leaves `sleep` undefined so
13609
+ * `withRetry`'s real schedule applies; a test injects a no-op so the ~47s
13610
+ * budget does not have to be waited out.
13611
+ */
13612
+ const autoScalingRetryDelays = {};
13613
+ /**
13614
+ * Resource ids per `DescribeScalableTargets` request. Kept at the API's
13615
+ * documented per-page ceiling rather than a larger guess — the request is
13616
+ * paginated anyway, and over-asking risks a `ValidationException` that would
13617
+ * degrade the probe to "presence unknown" for the whole region.
13618
+ */
13619
+ const DESCRIBE_SCALABLE_TARGETS_BATCH = 50;
13620
+ /**
13621
+ * Collect every application-autoscaling target a CFn GlobalTable properties
13622
+ * bag declares (Issue #1419).
13623
+ *
13624
+ * Where each dimension lives in the CFn shape — this is NOT symmetric, and
13625
+ * the asymmetry is the reason the per-index targets were missed:
13626
+ *
13627
+ * | dimension | CFn source | region |
13628
+ * | ------------------------------------ | ----------------------------------------------------------------------------- | ------ |
13629
+ * | `dynamodb:table:WriteCapacityUnits` | `WriteProvisionedThroughputSettings.WriteCapacityAutoScalingSettings` | local |
13630
+ * | `dynamodb:index:WriteCapacityUnits` | `GlobalSecondaryIndexes[].WriteProvisionedThroughputSettings.…` | local |
13631
+ * | `dynamodb:table:ReadCapacityUnits` | `Replicas[].ReadProvisionedThroughputSettings.ReadCapacityAutoScalingSettings` | replica's |
13632
+ * | `dynamodb:index:ReadCapacityUnits` | `Replicas[].GlobalSecondaryIndexes[].ReadProvisionedThroughputSettings.…` | replica's |
13633
+ *
13634
+ * WRITE capacity on a global table is owned by the source region only, so
13635
+ * both write dimensions register in the LOCAL region. READ capacity is
13636
+ * per-replica: each replica registers its own target with its own region's
13637
+ * application-autoscaling endpoint — including the LOCAL replica, whose
13638
+ * read target was never registered by any code path before this change
13639
+ * (`update()`'s replica loops all `continue` on the local region).
13640
+ *
13641
+ * A replica entry with no `Region` is skipped rather than defaulted: a
13642
+ * target registered against the wrong region's control plane is invisible
13643
+ * to every later diff and would leak on destroy.
13644
+ */
13645
+ function collectAutoScalingTargets(properties, localRegion) {
13646
+ const specs = [];
13647
+ const tableWrite = asRecord(asRecord(properties["WriteProvisionedThroughputSettings"])?.["WriteCapacityAutoScalingSettings"]);
13648
+ if (tableWrite) specs.push({
13649
+ dimension: "dynamodb:table:WriteCapacityUnits",
13650
+ region: localRegion,
13651
+ settings: tableWrite
13652
+ });
13653
+ const cfnIndexes = properties["GlobalSecondaryIndexes"];
13654
+ if (Array.isArray(cfnIndexes)) for (const entry of cfnIndexes) {
13655
+ const gsi = asRecord(entry);
13656
+ const indexName = gsi?.["IndexName"];
13657
+ if (!gsi || typeof indexName !== "string") continue;
13658
+ const indexWrite = asRecord(asRecord(gsi["WriteProvisionedThroughputSettings"])?.["WriteCapacityAutoScalingSettings"]);
13659
+ if (indexWrite) specs.push({
13660
+ dimension: "dynamodb:index:WriteCapacityUnits",
13661
+ indexName,
13662
+ region: localRegion,
13663
+ settings: indexWrite
13664
+ });
13665
+ }
13666
+ const replicas = properties["Replicas"];
13667
+ if (Array.isArray(replicas)) for (const replicaEntry of replicas) {
13668
+ const replica = asRecord(replicaEntry);
13669
+ const region = replica?.["Region"];
13670
+ if (!replica || typeof region !== "string") continue;
13671
+ const replicaRead = asRecord(asRecord(replica["ReadProvisionedThroughputSettings"])?.["ReadCapacityAutoScalingSettings"]);
13672
+ if (replicaRead) specs.push({
13673
+ dimension: "dynamodb:table:ReadCapacityUnits",
13674
+ region,
13675
+ settings: replicaRead
13676
+ });
13677
+ const replicaIndexes = replica["GlobalSecondaryIndexes"];
13678
+ if (!Array.isArray(replicaIndexes)) continue;
13679
+ for (const entry of replicaIndexes) {
13680
+ const replicaIndex = asRecord(entry);
13681
+ const indexName = replicaIndex?.["IndexName"];
13682
+ if (!replicaIndex || typeof indexName !== "string") continue;
13683
+ const indexRead = asRecord(asRecord(replicaIndex["ReadProvisionedThroughputSettings"])?.["ReadCapacityAutoScalingSettings"]);
13684
+ if (indexRead) specs.push({
13685
+ dimension: "dynamodb:index:ReadCapacityUnits",
13686
+ indexName,
13687
+ region,
13688
+ settings: indexRead
13689
+ });
13690
+ }
13691
+ }
13692
+ return specs;
13693
+ }
13392
13694
  /** Narrow an unknown CFn sub-object to a record (or `undefined`). */
13393
13695
  function asRecord(value) {
13394
13696
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
@@ -13417,38 +13719,48 @@ function toFiniteNumber(value) {
13417
13719
  * member — `WriteCapacityAutoScalingSettings` — i.e. write capacity on a
13418
13720
  * GlobalTable is ALWAYS auto-scaled; there is no literal `WriteCapacityUnits`
13419
13721
  * (that is why CDK 2.244's `TableV2` rejects `Capacity.fixed()` for write).
13420
- * `CreateTable` / `UpdateTable` still require a concrete starting number, so
13421
- * we take `SeedCapacity` ("the initial provisioned capacity units") when the
13422
- * template supplies it, else `MinCapacity`.
13722
+ * `CreateTable` / `UpdateTable` still require a concrete starting number;
13723
+ * which member of the auto-scaling block supplies it is context-dependent
13724
+ * (`MinCapacity` on a create, `SeedCapacity` on a billing-mode flip) — see
13725
+ * {@link CapacitySource}, which `source` selects.
13423
13726
  *
13424
13727
  * A literal `WriteCapacityUnits` is still honored first: the CFn schema
13425
13728
  * forbids it, but hand-authored / imported templates and cdkd state written
13426
13729
  * before this helper existed can carry it, and honoring it is strictly more
13427
13730
  * faithful than ignoring it.
13731
+ *
13732
+ * `source` selects between the two contexts CloudFormation distinguishes —
13733
+ * see {@link CapacitySource}. Defaults to `'min'`, the common case.
13428
13734
  */
13429
- function deriveWriteCapacityUnits(settings) {
13735
+ function deriveWriteCapacityUnits(settings, source = "min") {
13430
13736
  if (!settings) return void 0;
13431
13737
  const literal = toFiniteNumber(settings["WriteCapacityUnits"]);
13432
13738
  if (literal !== void 0) return literal;
13433
13739
  const autoScaling = asRecord(settings["WriteCapacityAutoScalingSettings"]);
13434
13740
  if (!autoScaling) return void 0;
13435
- return toFiniteNumber(autoScaling["SeedCapacity"]) ?? toFiniteNumber(autoScaling["MinCapacity"]);
13741
+ return pickAutoScalingCapacity(autoScaling, source);
13436
13742
  }
13437
13743
  /**
13438
13744
  * Derive a single READ capacity number from a CFn
13439
13745
  * `ReadProvisionedThroughputSettings` block (both the per-replica shape,
13440
13746
  * which carries `ReadCapacityUnits` + `ReadCapacityAutoScalingSettings`, and
13441
13747
  * the GSI/table-level `GlobalReadProvisionedThroughputSettings` shape, which
13442
- * carries only `ReadCapacityUnits`). Same Seed-before-Min precedence as the
13443
- * write side.
13748
+ * carries only `ReadCapacityUnits`). Same {@link CapacitySource} precedence
13749
+ * as the write side.
13444
13750
  */
13445
- function deriveReadCapacityUnits(settings) {
13751
+ function deriveReadCapacityUnits(settings, source = "min") {
13446
13752
  if (!settings) return void 0;
13447
13753
  const literal = toFiniteNumber(settings["ReadCapacityUnits"]);
13448
13754
  if (literal !== void 0) return literal;
13449
13755
  const autoScaling = asRecord(settings["ReadCapacityAutoScalingSettings"]);
13450
13756
  if (!autoScaling) return void 0;
13451
- return toFiniteNumber(autoScaling["SeedCapacity"]) ?? toFiniteNumber(autoScaling["MinCapacity"]);
13757
+ return pickAutoScalingCapacity(autoScaling, source);
13758
+ }
13759
+ /** Resolve a `*CapacityAutoScalingSettings` block to one number. */
13760
+ function pickAutoScalingCapacity(autoScaling, source) {
13761
+ const min = toFiniteNumber(autoScaling["MinCapacity"]);
13762
+ if (source === "min") return min ?? toFiniteNumber(autoScaling["SeedCapacity"]);
13763
+ return toFiniteNumber(autoScaling["SeedCapacity"]) ?? min;
13452
13764
  }
13453
13765
  /**
13454
13766
  * Translate the CFn `AWS::DynamoDB::GlobalTable` `GlobalSecondaryIndexes[]`
@@ -13487,7 +13799,7 @@ function deriveReadCapacityUnits(settings) {
13487
13799
  * written before this fix (and hand-authored templates) can carry them, and
13488
13800
  * silently re-deriving over an explicit value would be a regression.
13489
13801
  */
13490
- function toSdkGlobalSecondaryIndexes(properties, region, billingMode) {
13802
+ function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "min") {
13491
13803
  const rawIndexes = properties["GlobalSecondaryIndexes"];
13492
13804
  if (rawIndexes !== void 0 && !Array.isArray(rawIndexes)) throw new Error(`AWS::DynamoDB::GlobalTable GlobalSecondaryIndexes must be an array, got ${typeof rawIndexes} (${JSON.stringify(rawIndexes)?.slice(0, 200)}). An unresolved intrinsic here would otherwise deploy a table with no indexes.`);
13493
13805
  const cfnIndexes = rawIndexes ?? [];
@@ -13519,8 +13831,8 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode) {
13519
13831
  const explicitOnDemand = asRecord(gsi["OnDemandThroughput"]);
13520
13832
  if (explicitProvisioned) sdk.ProvisionedThroughput = explicitProvisioned;
13521
13833
  else if (billingMode === "PROVISIONED") sdk.ProvisionedThroughput = {
13522
- ReadCapacityUnits: deriveReadCapacityUnits(asRecord(localEntry?.["ReadProvisionedThroughputSettings"])) ?? deriveReadCapacityUnits(asRecord(gsi["ReadProvisionedThroughputSettings"])) ?? DEFAULT_CAPACITY_UNITS,
13523
- WriteCapacityUnits: deriveWriteCapacityUnits(asRecord(gsi["WriteProvisionedThroughputSettings"])) ?? DEFAULT_CAPACITY_UNITS
13834
+ ReadCapacityUnits: deriveReadCapacityUnits(asRecord(localEntry?.["ReadProvisionedThroughputSettings"]), source) ?? deriveReadCapacityUnits(asRecord(gsi["ReadProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS,
13835
+ WriteCapacityUnits: deriveWriteCapacityUnits(asRecord(gsi["WriteProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS
13524
13836
  };
13525
13837
  if (explicitOnDemand) sdk.OnDemandThroughput = explicitOnDemand;
13526
13838
  else if (billingMode !== "PROVISIONED") {
@@ -13536,6 +13848,89 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode) {
13536
13848
  return result;
13537
13849
  }
13538
13850
  /**
13851
+ * True when a CFn sub-object is nothing but an unresolved intrinsic
13852
+ * (`{"Fn::If": […]}` / `{"Ref": …}`), i.e. the template DID declare the block
13853
+ * but its value could not be resolved to a shape with members.
13854
+ *
13855
+ * Needed because presence is otherwise tested on the MEMBER key: an
13856
+ * intrinsic-valued block IS a record, so `asRecord` succeeds while
13857
+ * `['MaxReadRequestUnits']` is undefined — reporting "not declared" and firing
13858
+ * the destructive `-1` reset on a ceiling the template was trying to SET. That
13859
+ * is issue #1440 one level up from where the member test looks.
13860
+ */
13861
+ function isUnresolvedIntrinsicBlock(value) {
13862
+ const record = asRecord(value);
13863
+ if (!record) return false;
13864
+ const keys = Object.keys(record);
13865
+ return keys.length > 0 && keys.every((k) => k === "Ref" || k.startsWith("Fn::"));
13866
+ }
13867
+ /**
13868
+ * Per-index RAW presence of the two per-GSI on-demand members, keyed by
13869
+ * `IndexName` (issue #1440).
13870
+ *
13871
+ * The per-GSI reset needs to know "did the template DROP this member", and the
13872
+ * only faithful answer is whether the CFn key is THERE — not whether its value
13873
+ * coerced. `toSdkGlobalSecondaryIndexes` runs every value through
13874
+ * {@link toFiniteNumber}, which returns `undefined` for a present-but-
13875
+ * unparseable value (an unresolved `{Ref: …}`, `''`, an object). Deciding
13876
+ * "dropped" from that collapsed view sent the `-1` reset for a member the
13877
+ * template was actively trying to SET, silently clearing the ceiling — the same
13878
+ * silent-wrong-action class the reset exists to remove, and the exact hazard
13879
+ * the table-level path was corrected for in #1434's PR.
13880
+ *
13881
+ * So this walks the same two sources as `toSdkGlobalSecondaryIndexes` — the
13882
+ * top-level GSI for the write half, the LOCAL replica's index entry (with the
13883
+ * GSI-level spelling as the documented fallback) for the read half — and
13884
+ * reports presence only. Keeping it beside that function is deliberate: the
13885
+ * two must agree on where each member lives, and a divergence between them is
13886
+ * exactly what would re-open the bug.
13887
+ *
13888
+ * A missing entry means "not declared", which lets the reset FIRE — i.e. the
13889
+ * DESTRUCTIVE direction, not a conservative one. Two DIFFERENT mechanisms keep
13890
+ * that safe, and neither is "the translation validates everything":
13891
+ * - a non-array `GlobalSecondaryIndexes` is refused LOUDLY by
13892
+ * `toSdkGlobalSecondaryIndexes` before any of this runs;
13893
+ * - a malformed per-ENTRY shape is NOT refused — it is passed through
13894
+ * untouched so AWS surfaces the real error — but such an entry carries no
13895
+ * usable `IndexName`, so the reset loop skips it before consulting this map.
13896
+ * Do not relax either behavior on the assumption that this helper degrades
13897
+ * safely on its own; it does not.
13898
+ */
13899
+ function collectRawOnDemandDeclarations(properties, region) {
13900
+ const out = /* @__PURE__ */ new Map();
13901
+ const rawIndexes = properties["GlobalSecondaryIndexes"];
13902
+ if (!Array.isArray(rawIndexes)) return out;
13903
+ const replicas = properties["Replicas"];
13904
+ const localReplicaIndexes = asRecord(Array.isArray(replicas) ? replicas.find((r) => asRecord(r)?.["Region"] === region) : void 0)?.["GlobalSecondaryIndexes"];
13905
+ const localByName = /* @__PURE__ */ new Map();
13906
+ if (Array.isArray(localReplicaIndexes)) for (const entry of localReplicaIndexes) {
13907
+ const record = asRecord(entry);
13908
+ const name = record?.["IndexName"];
13909
+ if (record && typeof name === "string") localByName.set(name, record);
13910
+ }
13911
+ for (const entry of rawIndexes) {
13912
+ const gsi = asRecord(entry);
13913
+ const name = gsi?.["IndexName"];
13914
+ if (!gsi || typeof name !== "string") continue;
13915
+ const localEntry = localByName.get(name);
13916
+ const explicitOnDemand = asRecord(gsi["OnDemandThroughput"]);
13917
+ if (explicitOnDemand) {
13918
+ out.set(name, {
13919
+ readDeclared: explicitOnDemand["MaxReadRequestUnits"] !== void 0,
13920
+ writeDeclared: explicitOnDemand["MaxWriteRequestUnits"] !== void 0
13921
+ });
13922
+ continue;
13923
+ }
13924
+ const readBlocks = [localEntry?.["ReadOnDemandThroughputSettings"], gsi["ReadOnDemandThroughputSettings"]];
13925
+ const writeBlock = gsi["WriteOnDemandThroughputSettings"];
13926
+ out.set(name, {
13927
+ readDeclared: readBlocks.some((block) => asRecord(block)?.["MaxReadRequestUnits"] !== void 0 || isUnresolvedIntrinsicBlock(block)),
13928
+ writeDeclared: asRecord(writeBlock)?.["MaxWriteRequestUnits"] !== void 0 || isUnresolvedIntrinsicBlock(writeBlock)
13929
+ });
13930
+ }
13931
+ return out;
13932
+ }
13933
+ /**
13539
13934
  * Translate a CFn `Replicas[].GlobalSecondaryIndexes[]` blob (the
13540
13935
  * `ReplicaGlobalSecondaryIndexSpecification` shape) into the SDK's
13541
13936
  * `ReplicaGlobalSecondaryIndex[]` used by `CreateReplicationGroupMemberAction`
@@ -65020,7 +65415,7 @@ function createMigrateCommand() {
65020
65415
  */
65021
65416
  function buildProgram() {
65022
65417
  const program = new Command();
65023
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.18");
65418
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.20");
65024
65419
  program.hook("preAction", (_thisCommand, actionCommand) => {
65025
65420
  const { profile } = actionCommand.optsWithGlobals();
65026
65421
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;