@go-to-k/cdkd 0.283.27 → 0.283.28

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 coerceCfnBoolean, $t as resolveApp, A as cyan, An as LocalInvokeBuildError$1, At as createAssetRedirectResolver, B as findSilentDropProperties, Bn as StackHasActiveImportsError, Bt as validateContainerRepoName, C as makeCanonicalizePropertiesFn, Cn as resetAwsClients, Ct as S3StateBackend, D as renderStatefulReason, Dt as stringifyValue, E as isStatefulRecreateTargetSync, En as CdkdError, Et as AssetPublisher, F as IAMRoleProvider, Fn as NestedStackChildDirectDestroyError, Ft as BOOTSTRAP_MARKER_PREFIX, G as IntrinsicFunctionResolver, Gt as runDockerForeground, H as slowCcOperationTimeoutMs, Ht as buildDockerImage, I as collectInlinePolicyNamesManagedBySiblings, In as PartialFailureError, It as ensureAssetStorage, J as refStateLookupFromResource, Jn as isMarkedNonRetryable, Jt as getDockerImageBySourceHash, K as cfnRefValueFromPhysicalId, Kn as normalizeAwsError, Kt as runDockerStreaming, L as clearOnUpdateRemoval, Ln as ProvisioningError, Lt as getBootstrapMarkerKey, M as green, Mn as LocalStartServiceError, Mt as rewriteTemplateAssetReferences, N as red, Nt as escapeRegExp, O as formatResourceLine, Ot as WorkGraph, P as yellow, Pn as MissingCdkCliError, Pt as AssetModeResolver, Q as assertRegionMatch, Qt as getLegacyStateBucketName, R as ProviderRegistry, Rn as ResourceTimeoutError, Rt as parseBootstrapMarker, S as unsupportedFinalSnapshotError, Sn as getAwsClients, St as LockManager, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tt as shouldRetainResource, U as disableInstanceApiTermination, Ut as formatDockerLoginError, V as CloudControlProvider, Vn as StackTerminationProtectionError, Vt as buildDenyExternalAccessPolicy, W as isTerminationProtectionPropagationError, Wt as getDockerCmd, X as normalizeAwsTagsToCfn, Xn as isThrottlingError, Xt as synthesisStatusMessage, Y as WAFv2WebACLProvider, Yn as isRetryableTransientError, Yt as Synthesizer, Z as resolveExplicitPhysicalId, Zn as markNonRetryable, Zt as getDefaultStateBucketName, _ as buildFinalSnapshotIdentifier, _n as AssemblyReader, _t as INTRINSIC_KEYS, a as DeploymentEventsStore, an as resolveUseCdkBootstrapAssets, at as requireConfigObject, b as isFinalSnapshotError, bn as resolveBucketRegion, bt as DagBuilder, c as replayFailedOperations, cn as CFN_TEMPLATE_BODY_LIMIT, ct as redactSecretsForState, d as deleteSkipReason, dn as findLargeInlineResources, dt as s3BucketDomainName, en as resolveAutoAssetStorage, et as configBooleanRefusal, f as withResourceDeadline, fn as uploadCfnTemplate, ft as s3BucketDualStackDomainName, g as PRE_DELETE_SNAPSHOT_TYPES, gn as derivePartitionAndUrlSuffix, gt as DiffCalculator, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as canonicalizeRegion, ht as applyRoleArnIfSet, i as DeploymentEventsReader, in as resolveStateBucketWithDefaultAndSource, it as requireConfigArray, j as gray, jn as LocalMigrateError, jt as loadPublishableAssetManifest, k as bold, kn as DeployCancelledError, kt as buildAssetRedirectMap, l as replayRollback, ln as CFN_TEMPLATE_URL_LIMIT, lt as scrubResourceRecord, m as computeImplicitDeleteEdges, mn as PARTITION_TABLE, mt as s3BucketWebsiteUrl, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as resolveSkipPrefix, nt as readConfigString, o as planFailedOps, on as stateBucketExistenceConfirmed, ot as requireConfigString, p as IMPLICIT_DELETE_DEPENDENCIES, pn as expectedOwnerParam, pt as s3BucketRegionalDomainName, q as getAccountInfo, qn as withErrorHandling, qt as AssetManifestLoader, r as DeployEngine, rn as resolveStateBucketWithDefault, rt as replayWarn, s as planRollback, sn as warnDeprecatedNoPrefixCliFlag, st as TEMPLATE_SOURCED_READBACK_RULES, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveCaptureObservedState, tt as configStringRefusal, u as UNSPECIFIED_SKIP_REASON, un as MIGRATE_TMP_PREFIX, ut as s3BucketArn, v as ccRoutedFinalSnapshotError, vn as processStackMessages, vt as describeTypeWithThrottleRetry, w as extractDeploymentEventError, wn as setAwsClients, wt as rebuildClientForBucketRegion, x as refusesFinalSnapshot, xn as AwsClients, xt as TemplateParser, y as createPreDeleteFinalSnapshot, yt as withRetry, z as findActionableSilentDrops, zn as ResourceUpdateNotSupportedError, zt as validateAssetBucketName } from "./deploy-engine-CsvKYG0d.js";
2
+ import { $ as coerceCfnBoolean, $t as resolveApp, A as cyan, An as LocalInvokeBuildError$1, At as createAssetRedirectResolver, B as findSilentDropProperties, Bn as StackHasActiveImportsError, Bt as validateContainerRepoName, C as makeCanonicalizePropertiesFn, Cn as resetAwsClients, Ct as S3StateBackend, D as renderStatefulReason, Dt as stringifyValue, E as isStatefulRecreateTargetSync, En as CdkdError, Et as AssetPublisher, F as IAMRoleProvider, Fn as NestedStackChildDirectDestroyError, Ft as BOOTSTRAP_MARKER_PREFIX, G as IntrinsicFunctionResolver, Gt as runDockerForeground, H as slowCcOperationTimeoutMs, Ht as buildDockerImage, I as collectInlinePolicyNamesManagedBySiblings, In as PartialFailureError, It as ensureAssetStorage, J as refStateLookupFromResource, Jn as isMarkedNonRetryable, Jt as getDockerImageBySourceHash, K as cfnRefValueFromPhysicalId, Kn as normalizeAwsError, Kt as runDockerStreaming, L as clearOnUpdateRemoval, Ln as ProvisioningError, Lt as getBootstrapMarkerKey, M as green, Mn as LocalStartServiceError, Mt as rewriteTemplateAssetReferences, N as red, Nt as escapeRegExp, O as formatResourceLine, Ot as WorkGraph, P as yellow, Pn as MissingCdkCliError, Pt as AssetModeResolver, Q as assertRegionMatch, Qt as getLegacyStateBucketName, R as ProviderRegistry, Rn as ResourceTimeoutError, Rt as parseBootstrapMarker, S as unsupportedFinalSnapshotError, Sn as getAwsClients, St as LockManager, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tt as shouldRetainResource, U as disableInstanceApiTermination, Ut as formatDockerLoginError, V as CloudControlProvider, Vn as StackTerminationProtectionError, Vt as buildDenyExternalAccessPolicy, W as isTerminationProtectionPropagationError, Wt as getDockerCmd, X as normalizeAwsTagsToCfn, Xn as isThrottlingError, Xt as synthesisStatusMessage, Y as WAFv2WebACLProvider, Yn as isRetryableTransientError, Yt as Synthesizer, Z as resolveExplicitPhysicalId, Zn as markNonRetryable, Zt as getDefaultStateBucketName, _ as buildFinalSnapshotIdentifier, _n as AssemblyReader, _t as INTRINSIC_KEYS, a as DeploymentEventsStore, an as resolveUseCdkBootstrapAssets, at as requireConfigObject, b as isFinalSnapshotError, bn as resolveBucketRegion, bt as DagBuilder, c as replayFailedOperations, cn as CFN_TEMPLATE_BODY_LIMIT, ct as redactSecretsForState, d as deleteSkipReason, dn as findLargeInlineResources, dt as s3BucketDomainName, en as resolveAutoAssetStorage, et as configBooleanRefusal, f as withResourceDeadline, fn as uploadCfnTemplate, ft as s3BucketDualStackDomainName, g as PRE_DELETE_SNAPSHOT_TYPES, gn as derivePartitionAndUrlSuffix, gt as DiffCalculator, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as canonicalizeRegion, ht as applyRoleArnIfSet, i as DeploymentEventsReader, in as resolveStateBucketWithDefaultAndSource, it as requireConfigArray, j as gray, jn as LocalMigrateError, jt as loadPublishableAssetManifest, k as bold, kn as DeployCancelledError, kt as buildAssetRedirectMap, l as replayRollback, ln as CFN_TEMPLATE_URL_LIMIT, lt as scrubResourceRecord, m as computeImplicitDeleteEdges, mn as PARTITION_TABLE, mt as s3BucketWebsiteUrl, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as resolveSkipPrefix, nt as readConfigString, o as planFailedOps, on as stateBucketExistenceConfirmed, ot as requireConfigString, p as IMPLICIT_DELETE_DEPENDENCIES, pn as expectedOwnerParam, pt as s3BucketRegionalDomainName, q as getAccountInfo, qn as withErrorHandling, qt as AssetManifestLoader, r as DeployEngine, rn as resolveStateBucketWithDefault, rt as replayWarn, s as planRollback, sn as warnDeprecatedNoPrefixCliFlag, st as TEMPLATE_SOURCED_READBACK_RULES, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveCaptureObservedState, tt as configStringRefusal, u as UNSPECIFIED_SKIP_REASON, un as MIGRATE_TMP_PREFIX, ut as s3BucketArn, v as ccRoutedFinalSnapshotError, vn as processStackMessages, vt as describeTypeWithThrottleRetry, w as extractDeploymentEventError, wn as setAwsClients, wt as rebuildClientForBucketRegion, x as refusesFinalSnapshot, xn as AwsClients, xt as TemplateParser, y as createPreDeleteFinalSnapshot, yt as withRetry, z as findActionableSilentDrops, zn as ResourceUpdateNotSupportedError, zt as validateAssetBucketName } from "./deploy-engine-5rA6lMn8.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-uNJDVURJ.js";
4
+ import { t as ASGProvider } from "./asg-provider-DzaowTd_.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";
@@ -13989,6 +13989,209 @@ function isRecord$1(value) {
13989
13989
  return typeof value === "object" && value !== null && !Array.isArray(value);
13990
13990
  }
13991
13991
 
13992
+ //#endregion
13993
+ //#region src/provisioning/dynamodb-warm-throughput.ts
13994
+ /**
13995
+ * The two DynamoDB `WarmThroughput` rules that `AWS::DynamoDB::Table` and
13996
+ * `AWS::DynamoDB::GlobalTable` both need, in ONE spelling.
13997
+ *
13998
+ * `WarmThroughput` is the same CloudFormation block on both types, and both
13999
+ * providers have to answer the same two questions about it before an
14000
+ * `UpdateTable` / `CreateTable` goes out:
14001
+ *
14002
+ * 1. **Is this block SENDABLE, and as what numbers?** CloudFormation is
14003
+ * stringly typed, so `{ReadUnitsPerSecond: '12000'}` — or anything that
14004
+ * came back from an `Fn::Sub` — arrives as a STRING and must not be
14005
+ * forwarded verbatim into a numeric `Long` field.
14006
+ * 2. **Would sending it LOWER what AWS already reports?** Warm throughput
14007
+ * only ever rises with a table's traffic and AWS REJECTS a call that
14008
+ * lowers it (`decreasing WarmThroughput is not supported`, measured live
14009
+ * us-east-1 2026-08-13 for issue #1768).
14010
+ *
14011
+ * **Provenance, stated precisely because it is easy to over-claim.** Exactly
14012
+ * ONE of these rules ever shipped: `AWS::DynamoDB::Table` got them in PR #1808
14013
+ * (issues #1760 / #1768), and the `AWS::DynamoDB::GlobalTable` side never
14014
+ * existed outside the change that created this file (issue #1857). So this is
14015
+ * the Table rule LIFTED — not two shipped rules reconciled, and no deployed
14016
+ * behaviour changed for either type when it moved here.
14017
+ *
14018
+ * What WAS compared is the lifted rule against the GlobalTable spelling
14019
+ * drafted alongside it, over the shapes both had to answer (a quoted numeric
14020
+ * string, a partially usable block, a mixed decrease/increase, an absent live
14021
+ * value, a zero, a negative, a non-numeric string, a whitespace string, an
14022
+ * explicit `undefined` / `null`, a boolean, an empty block, a scalar, an
14023
+ * array). {@link isWarmThroughputDecrease} agreed on every one; the coercion
14024
+ * agreed on all but a whitespace-only string, where a bare `Number(' ')` is
14025
+ * `0` rather than `NaN`. This module keeps the REFUSING answer for that shape,
14026
+ * since `' '` is not a capacity anyone declared and a warm throughput of 0
14027
+ * is not a request AWS can honour. That is a DRAFT reconciled against a
14028
+ * shipped rule, which is worth less than two shipped rules agreeing — the
14029
+ * reason to read the probe list as a design note rather than as field
14030
+ * evidence.
14031
+ *
14032
+ * Living here rather than in either provider is the point. A decrease guard is
14033
+ * three clauses that all FAIL OPEN for different reasons, and two files
14034
+ * spelling it independently is two chances for a later "fix" to change one of
14035
+ * them — at which point the sibling type silently keeps the old answer, and
14036
+ * nothing in the tree says which is intended. Same class as
14037
+ * `emr-configuration.ts`, and the same reason.
14038
+ *
14039
+ * Deliberately NOT here: everything only ONE provider has. The `Table` side's
14040
+ * `isSendableWarmThroughput` / `isRefusedWarmThroughput` /
14041
+ * `declaresWarmThroughput` / `warmThroughputAlreadyMatches` are its drift-side
14042
+ * and already-matches gates, which `GlobalTable` has no counterpart to (issue
14043
+ * #1742 strips the per-index `WarmThroughput` from BOTH of its drift
14044
+ * comparison sides unconditionally, so drift never asks the question there);
14045
+ * the `GlobalTable` side's `warmThroughputDiagnostic` builds a
14046
+ * `ThroughputDiagnostic` that only that provider's collector understands.
14047
+ * Moving a helper with one caller here would buy nothing and cost a hop.
14048
+ *
14049
+ * Issues: #1760 / #1768 (Table, PR #1808), #1857 (GlobalTable).
14050
+ */
14051
+ /**
14052
+ * The two `WarmThroughput` members, in the ONE order every message, every
14053
+ * comparison and every emitted block uses. A shared order is what makes two
14054
+ * blocks carrying the same numbers compare equal by `deepEqual` and serialize
14055
+ * to the same wire bytes regardless of the order the template wrote them in.
14056
+ */
14057
+ const WARM_THROUGHPUT_MEMBERS = ["ReadUnitsPerSecond", "WriteUnitsPerSecond"];
14058
+ /**
14059
+ * A CloudFormation-borne numeric property, or `undefined` when the value is
14060
+ * not a usable number.
14061
+ *
14062
+ * Plain `Number()` coercion is NOT good enough and the tree learned it three
14063
+ * separate times: `Number(null)`, `Number('')`, `Number([])`, `Number(false)`
14064
+ * and `Number(' ')` are all **0**, not `NaN` — so a live `0` would compare
14065
+ * EQUAL to a desired `null` / `''` / `[]` / `false`, and a whitespace-only
14066
+ * string would be forwarded as a request for zero units.
14067
+ *
14068
+ * A YAML-borne numeric STRING is still accepted, because that is a real
14069
+ * template shape and `'12000'` genuinely means 12000.
14070
+ *
14071
+ * EXPORTED, and not because warm throughput needs it exported. This exact rule
14072
+ * was hand-written three times — here, as `dynamodb-table-provider.ts`'s
14073
+ * `capacityNumber` (byte-identical), and as `dynamodb-globaltable-provider.ts`'s
14074
+ * `toFiniteNumber` (a different spelling of the same total function) — which is
14075
+ * precisely the divergence this module exists to stop: the next "fix" to one of
14076
+ * them would have left the other two answering differently, with nothing in the
14077
+ * tree saying which was intended. It reads `ReadCapacityUnits` /
14078
+ * `MaxReadRequestUnits` / `MinCapacity` as readily as it reads
14079
+ * `ReadUnitsPerSecond`; there is only ever one right answer for "is this
14080
+ * stringly-typed CFn value a number I can send?".
14081
+ */
14082
+ function toFiniteNumber(value) {
14083
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
14084
+ if (typeof value === "string" && value.trim() !== "") {
14085
+ const n = Number(value);
14086
+ return Number.isFinite(n) ? n : void 0;
14087
+ }
14088
+ }
14089
+ /**
14090
+ * Coerce a CFn `WarmThroughput` block to the numeric shape the SDK's `Long`
14091
+ * fields accept, PER MEMBER.
14092
+ *
14093
+ * PER MEMBER, not whole-block, and that distinction is the point: a block
14094
+ * whose write half is an unresolved intrinsic still has a perfectly good read
14095
+ * half, and dropping both would silently discard a value the template really
14096
+ * did ask for. The dropped member is NAMED (`droppedMembers`) so the caller's
14097
+ * warning can say which half went missing instead of reporting the whole
14098
+ * property.
14099
+ *
14100
+ * A block with NO usable member yields `spec: undefined` — refused rather than
14101
+ * forwarded, because forwarding a malformed block surfaces as an opaque AWS
14102
+ * validation error naming neither cdkd nor the property. `droppedMembers` is
14103
+ * still populated in that case, so a REFUSAL message can name what it refused;
14104
+ * a block that is not an object at all (an unresolved `Fn::If`, a scalar, an
14105
+ * array) has no member to name and reports none, which is what lets a caller
14106
+ * word "one half went missing" differently from "the block is unusable".
14107
+ *
14108
+ * Pure: takes the raw bag, returns numbers, logs nothing. Each caller owns its
14109
+ * own message, so the wording stays consistent across that provider's send
14110
+ * sites without forcing one wording across both providers.
14111
+ */
14112
+ function coerceWarmThroughput$1(raw) {
14113
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { droppedMembers: [] };
14114
+ const bag = raw;
14115
+ const spec = {};
14116
+ const droppedMembers = [];
14117
+ for (const member of WARM_THROUGHPUT_MEMBERS) {
14118
+ if (bag[member] === void 0) continue;
14119
+ const coerced = toFiniteNumber(bag[member]);
14120
+ if (coerced === void 0) {
14121
+ droppedMembers.push(member);
14122
+ continue;
14123
+ }
14124
+ spec[member] = coerced;
14125
+ }
14126
+ if (Object.keys(spec).length === 0) return { droppedMembers };
14127
+ return {
14128
+ spec,
14129
+ droppedMembers
14130
+ };
14131
+ }
14132
+ /**
14133
+ * Whether the COERCED desired `WarmThroughput` would LOWER what AWS already
14134
+ * reports.
14135
+ *
14136
+ * Measured live (us-east-1, 2026-08-13, issue #1768) against a table AWS
14137
+ * reports `{ReadUnitsPerSecond: 12000, WriteUnitsPerSecond: 4000}` for:
14138
+ *
14139
+ * ```
14140
+ * UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000, WriteUnitsPerSecond: 2000}
14141
+ * ValidationException: One or more parameter values were invalid: Requested
14142
+ * ReadUnitsPerSecond for WarmThroughput for table is lower than current
14143
+ * WarmThroughput, decreasing WarmThroughput is not supported
14144
+ * UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000} -> same rejection
14145
+ * UpdateTable WarmThroughput={WriteUnitsPerSecond: 2000} -> same rejection, naming WriteUnitsPerSecond
14146
+ * UpdateTable WarmThroughput={12000, 4000} (re-assert) -> ACCEPTED
14147
+ * ```
14148
+ *
14149
+ * So the value is one AWS raises with the table's traffic and never lowers,
14150
+ * and a decrease is REJECTED rather than accepted-and-ignored — which is what
14151
+ * had to be measured before choosing, because the two produce different
14152
+ * correct answers. The caller SKIPS the call on a true here.
14153
+ *
14154
+ * Evaluated on the COERCED spec, never on the raw bag: analysing the raw bag
14155
+ * makes the verdict describe a request that is not the one being sent (a
14156
+ * dropped member is not part of the call and must not be part of the
14157
+ * comparison).
14158
+ *
14159
+ * Semantics, all three chosen to FAIL OPEN — i.e. to let the call through and
14160
+ * leave AWS as the authority — because a false positive here silently drops a
14161
+ * legitimate INCREASE, which is a real capacity change the user asked for,
14162
+ * while a false negative merely reproduces the pre-fix behaviour of an
14163
+ * AWS-side rejection that names the property:
14164
+ * - DECLARED members only. An absent member is not a request to lower
14165
+ * anything, so it takes no part in the verdict.
14166
+ * - MIXED is not a decrease. One member below live and the other above means
14167
+ * the call carries a genuine increase; AWS decides.
14168
+ * - An absent or unusable LIVE counterpart is not a decrease. Without a
14169
+ * number to compare against there is no evidence of one. Only the LIVE side
14170
+ * can reach that arm: the desired side is a COERCED spec, so a malformed
14171
+ * template value has already been dropped by {@link coerceWarmThroughput}.
14172
+ *
14173
+ * A decrease therefore requires every declared member to be at-or-below live
14174
+ * AND at least one to be strictly below.
14175
+ *
14176
+ * The skip this drives is right for EVERY `update()` caller — the deploy
14177
+ * engine, `cdkd drift --revert`, and the rollback executor's two revert arms —
14178
+ * because none of them can make AWS lower the value, so none loses anything a
14179
+ * doomed call would have achieved.
14180
+ */
14181
+ function isWarmThroughputDecrease(desired, live) {
14182
+ if (desired === void 0 || live === void 0) return false;
14183
+ let sawDecrease = false;
14184
+ for (const member of WARM_THROUGHPUT_MEMBERS) {
14185
+ if (desired[member] === void 0) continue;
14186
+ const wanted = toFiniteNumber(desired[member]);
14187
+ const current = toFiniteNumber(live[member]);
14188
+ if (wanted === void 0 || current === void 0) return false;
14189
+ if (wanted > current) return false;
14190
+ if (wanted < current) sawDecrease = true;
14191
+ }
14192
+ return sawDecrease;
14193
+ }
14194
+
13992
14195
  //#endregion
13993
14196
  //#region src/provisioning/providers/dynamodb-table-provider.ts
13994
14197
  /**
@@ -14091,28 +14294,6 @@ function hasUsableTableCapacity(value) {
14091
14294
  return true;
14092
14295
  }
14093
14296
  /**
14094
- * A capacity member, or `undefined` when the value is not a usable number.
14095
- *
14096
- * `Number()` coercion is NOT good enough here and the first draft of this
14097
- * helper shipped that bug: `Number(null)`, `Number('')`, `Number([])` and
14098
- * `Number(false)` are all **0**, not `NaN` — so a live `0` would have compared
14099
- * EQUAL to a desired `null` / `''` / `[]` / `false` and suppressed the call.
14100
- * The blast radius on DynamoDB is small (a PROVISIONED index's capacity is
14101
- * always >= 1), but the rule this encodes is copied to other providers, so it
14102
- * is spelled correctly rather than relying on the type that happens to be
14103
- * unreachable today.
14104
- *
14105
- * A YAML-borne numeric STRING is still accepted, because that is a real
14106
- * template shape and `'5'` genuinely means 5.
14107
- */
14108
- function capacityNumber(value) {
14109
- if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
14110
- if (typeof value === "string" && value.trim() !== "") {
14111
- const n = Number(value);
14112
- return Number.isFinite(n) ? n : void 0;
14113
- }
14114
- }
14115
- /**
14116
14297
  * Does the capacity AWS currently holds for a GSI already equal the pair the
14117
14298
  * template requests? (issue #1630)
14118
14299
  *
@@ -14131,10 +14312,10 @@ function capacityNumber(value) {
14131
14312
  */
14132
14313
  function liveCapacityAlreadyMatches(live, requested) {
14133
14314
  if (live === void 0) return false;
14134
- const liveRead = capacityNumber(live.ReadCapacityUnits);
14135
- const liveWrite = capacityNumber(live.WriteCapacityUnits);
14136
- const wantRead = capacityNumber(requested.ReadCapacityUnits);
14137
- const wantWrite = capacityNumber(requested.WriteCapacityUnits);
14315
+ const liveRead = toFiniteNumber(live.ReadCapacityUnits);
14316
+ const liveWrite = toFiniteNumber(live.WriteCapacityUnits);
14317
+ const wantRead = toFiniteNumber(requested.ReadCapacityUnits);
14318
+ const wantWrite = toFiniteNumber(requested.WriteCapacityUnits);
14138
14319
  if (liveRead === void 0 || liveWrite === void 0) return false;
14139
14320
  if (wantRead === void 0 || wantWrite === void 0) return false;
14140
14321
  return liveRead === wantRead && liveWrite === wantWrite;
@@ -14182,8 +14363,10 @@ const BILLING_FLIP_ACTIVE_WAIT_ATTEMPTS = 600;
14182
14363
  * Still not `!== undefined`, and still ONE spelling for the write sites and the
14183
14364
  * drift side both: a value this refuses is a value cdkd does not send, so the
14184
14365
  * readback must not emit AWS's computed value as though the template had asked
14185
- * for it. A YAML-borne numeric STRING is accepted, via {@link capacityNumber},
14186
- * because `'12000'` genuinely means 12000.
14366
+ * for it. A YAML-borne numeric STRING is accepted, by the shared coercion's own
14367
+ * number reader ({@link toFiniteNumber}, in `../dynamodb-warm-throughput.ts` —
14368
+ * the one this file's capacity paths read too), because `'12000'` genuinely
14369
+ * means 12000.
14187
14370
  */
14188
14371
  function isSendableWarmThroughput(value) {
14189
14372
  return coerceWarmThroughput(value) !== void 0;
@@ -14204,8 +14387,9 @@ function isSendableWarmThroughput(value) {
14204
14387
  * over), and it makes the quoted form WORK instead of merely warning.
14205
14388
  *
14206
14389
  * Per MEMBER, so a bad one cannot take a good one with it, and never `NaN`:
14207
- * {@link capacityNumber} accepts a number or a numeric string and rejects
14208
- * everything else, so a member that does not resolve is OMITTED from the spec
14390
+ * the shared coercion's number reader is {@link toFiniteNumber} — a number or
14391
+ * a numeric string, everything else rejected — so a member that
14392
+ * does not resolve is OMITTED from the spec
14209
14393
  * and reported in `dropped` for the caller to announce. AWS accepts a
14210
14394
  * one-member `WarmThroughput` (measured us-east-1, 2026-08-13: an
14211
14395
  * `UpdateTable` carrying only `ReadUnitsPerSecond` is a valid request shape —
@@ -14244,27 +14428,22 @@ function isSendableWarmThroughput(value) {
14244
14428
  * the comparator's `deepEqual` is strict about that. It is the generic
14245
14429
  * stringly-typed-CFn class (every numeric property has it), not something this
14246
14430
  * coercion can reach from the write side.
14431
+ *
14432
+ * The RULE moved to `../dynamodb-warm-throughput.ts` when
14433
+ * `AWS::DynamoDB::GlobalTable` needed the same one (issue #1857); this is the
14434
+ * local adapter onto the shape this file's four call sites already read, so
14435
+ * the behaviour, the signature and the tests are unchanged. The shared
14436
+ * coercion always reports `droppedMembers`, including on a total refusal —
14437
+ * that arm is unreachable from here, since a `usable === 0` outcome maps to
14438
+ * `undefined` and the names go with it, which is exactly what this file did
14439
+ * before.
14247
14440
  */
14248
14441
  function coerceWarmThroughput(value) {
14249
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
14250
- const bag = value;
14251
- const spec = {};
14252
- const dropped = [];
14253
- let usable = 0;
14254
- for (const member of ["ReadUnitsPerSecond", "WriteUnitsPerSecond"]) {
14255
- if (bag[member] === void 0) continue;
14256
- const n = capacityNumber(bag[member]);
14257
- if (n === void 0) {
14258
- dropped.push(member);
14259
- continue;
14260
- }
14261
- spec[member] = n;
14262
- usable++;
14263
- }
14264
- if (usable === 0) return void 0;
14442
+ const { spec, droppedMembers } = coerceWarmThroughput$1(value);
14443
+ if (spec === void 0) return void 0;
14265
14444
  return {
14266
14445
  spec,
14267
- dropped
14446
+ dropped: [...droppedMembers]
14268
14447
  };
14269
14448
  }
14270
14449
  /**
@@ -14337,60 +14516,6 @@ function declaresWarmThroughput(properties) {
14337
14516
  return properties !== void 0 && isSendableWarmThroughput(properties["WarmThroughput"]);
14338
14517
  }
14339
14518
  /**
14340
- * Is the WarmThroughput this update would SEND a DECREASE of what AWS already
14341
- * holds (issue #1768)?
14342
- *
14343
- * Measured live (us-east-1, 2026-08-13) against a table AWS reports
14344
- * `{ReadUnitsPerSecond: 12000, WriteUnitsPerSecond: 4000}` for:
14345
- *
14346
- * ```
14347
- * UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000, WriteUnitsPerSecond: 2000}
14348
- * ValidationException: One or more parameter values were invalid: Requested
14349
- * ReadUnitsPerSecond for WarmThroughput for table is lower than current
14350
- * WarmThroughput, decreasing WarmThroughput is not supported
14351
- * UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000} -> same rejection
14352
- * UpdateTable WarmThroughput={WriteUnitsPerSecond: 2000} -> same rejection, naming WriteUnitsPerSecond
14353
- * UpdateTable WarmThroughput={12000, 4000} (re-assert) -> ACCEPTED
14354
- * ```
14355
- *
14356
- * So the value is one AWS raises with the table's traffic and never lowers,
14357
- * and a decrease is REJECTED rather than accepted-and-ignored — which is what
14358
- * the issue said to measure before choosing, because the two produce different
14359
- * correct answers. The caller SKIPS the call on a true here.
14360
- *
14361
- * Rules, each one chosen so the skip is right for EVERY `update()` caller
14362
- * (the deploy engine, `cdkd drift --revert`, and the rollback executor's two
14363
- * revert arms — none of which can make AWS lower the value, so none of them
14364
- * loses anything a doomed call would have achieved):
14365
- *
14366
- * - Only the members the desired side DECLARES are examined; an omitted member
14367
- * is not sent and cannot be a decrease.
14368
- * - EVERY declared member must be at-or-below its live counterpart AND at
14369
- * least one strictly below. A MIXED request (one member up, one down) still
14370
- * goes out: AWS rejects it by name, which is the pre-fix behavior and better
14371
- * than silently dropping the increase the user asked for.
14372
- * - Anything not resolvable to a finite number fails OPEN (the call is still
14373
- * issued): the worst case is the pre-fix behavior, whereas a false positive
14374
- * would silently drop a real increase. Only the LIVE side can actually reach
14375
- * that arm now (PR review round 8): the desired side is a COERCED spec, so a
14376
- * malformed template value or an unresolved intrinsic has already been
14377
- * dropped by {@link coerceWarmThroughput} and cannot arrive here. The check
14378
- * is kept for the live side, which is whatever `DescribeTable` returned.
14379
- */
14380
- function isWarmThroughputDecrease(desired, live) {
14381
- if (live === void 0) return false;
14382
- let sawDecrease = false;
14383
- for (const member of ["ReadUnitsPerSecond", "WriteUnitsPerSecond"]) {
14384
- if (desired[member] === void 0) continue;
14385
- const wanted = capacityNumber(desired[member]);
14386
- const current = capacityNumber(live[member]);
14387
- if (wanted === void 0 || current === void 0) return false;
14388
- if (wanted > current) return false;
14389
- if (wanted < current) sawDecrease = true;
14390
- }
14391
- return sawDecrease;
14392
- }
14393
- /**
14394
14519
  * Does the warm throughput AWS currently holds already equal what this update
14395
14520
  * would request (issue #1768 PR review)?
14396
14521
  *
@@ -14419,10 +14544,10 @@ function isWarmThroughputDecrease(desired, live) {
14419
14544
  function warmThroughputAlreadyMatches(desired, live) {
14420
14545
  if (live === void 0) return false;
14421
14546
  let compared = 0;
14422
- for (const member of ["ReadUnitsPerSecond", "WriteUnitsPerSecond"]) {
14547
+ for (const member of WARM_THROUGHPUT_MEMBERS) {
14423
14548
  if (desired[member] === void 0) continue;
14424
- const wanted = capacityNumber(desired[member]);
14425
- const current = capacityNumber(live[member]);
14549
+ const wanted = toFiniteNumber(desired[member]);
14550
+ const current = toFiniteNumber(live[member]);
14426
14551
  if (wanted === void 0 || current === void 0) return false;
14427
14552
  if (wanted !== current) return false;
14428
14553
  compared++;
@@ -15529,7 +15654,7 @@ var DynamoDBTableProvider = class {
15529
15654
  */
15530
15655
  skipZeroCapacityIndexUpdate(indexName, physicalId, requested) {
15531
15656
  if (requested === void 0) return false;
15532
- if (capacityNumber(requested.ReadCapacityUnits) !== 0 || capacityNumber(requested.WriteCapacityUnits) !== 0) return false;
15657
+ if (toFiniteNumber(requested.ReadCapacityUnits) !== 0 || toFiniteNumber(requested.WriteCapacityUnits) !== 0) return false;
15533
15658
  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.`);
15534
15659
  return true;
15535
15660
  }
@@ -16425,6 +16550,35 @@ var DynamoDBGlobalTableProvider = class {
16425
16550
  }
16426
16551
  }
16427
16552
  /**
16553
+ * The `WarmThroughput` block to put on ONE `GlobalSecondaryIndexUpdate`, or
16554
+ * `undefined` when it must not be sent (issue #1857).
16555
+ *
16556
+ * ONE helper for all three `UpdateTable` send sites — the billing-flip
16557
+ * ride-along, the `added` loop's `Create`, and the `modified` loop's
16558
+ * `Update` — because three copies of a decrease guard is three chances to
16559
+ * word one differently, and the whole point of {@link isWarmThroughputDecrease}
16560
+ * is that the answer does not depend on which site asked.
16561
+ *
16562
+ * The `desired` argument is already COERCED (`toSdkGlobalSecondaryIndexes`
16563
+ * did it), so an unusable block never reaches here at all: it was refused
16564
+ * and reported during translation, and arrives as `undefined`. What is left
16565
+ * for this helper is the one question translation cannot answer, because it
16566
+ * needs live AWS state: would this call LOWER what AWS reports?
16567
+ *
16568
+ * The refusal is a WARN and a dropped member, not a throw. A decrease is a
16569
+ * template that has fallen behind a value AWS grew on its own; failing the
16570
+ * deploy over it would block every unrelated change to the same table, and
16571
+ * there is no template edit that "fixes" it other than raising a number the
16572
+ * user never chose.
16573
+ */
16574
+ warmThroughputForSend(indexName, desired, liveWarmByIndexName, logicalId, physicalId) {
16575
+ if (!desired) return void 0;
16576
+ const live = liveWarmByIndexName.get(indexName);
16577
+ if (!isWarmThroughputDecrease(desired, live)) return desired;
16578
+ const format = (spec) => WARM_THROUGHPUT_MEMBERS.filter((m) => spec?.[m] !== void 0).map((m) => `${m}=${spec?.[m]}`).join(", ");
16579
+ 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.`);
16580
+ }
16581
+ /**
16428
16582
  * Add a single replica region. Issues one `UpdateTableCommand` with
16429
16583
  * `ReplicaUpdates: [{ Create: { RegionName, ... } }]` and polls
16430
16584
  * `DescribeTable` until the replica's `ReplicaStatus` flips to ACTIVE.
@@ -16510,6 +16664,16 @@ var DynamoDBGlobalTableProvider = class {
16510
16664
  return (props["Replicas"] ?? []).find((r) => r["Region"] === currentRegion)?.["Tags"];
16511
16665
  };
16512
16666
  if (tableArn) await this.applyTagDiff(tableArn, extractLocalTags(previousProperties), extractLocalTags(properties));
16667
+ const liveWarmByIndexName = /* @__PURE__ */ new Map();
16668
+ for (const liveGsi of describeResp.Table?.GlobalSecondaryIndexes ?? []) {
16669
+ const name = liveGsi.IndexName;
16670
+ const warm = liveGsi.WarmThroughput;
16671
+ if (typeof name !== "string" || !warm) continue;
16672
+ const spec = {};
16673
+ if (warm.ReadUnitsPerSecond !== void 0) spec.ReadUnitsPerSecond = warm.ReadUnitsPerSecond;
16674
+ if (warm.WriteUnitsPerSecond !== void 0) spec.WriteUnitsPerSecond = warm.WriteUnitsPerSecond;
16675
+ if (Object.keys(spec).length > 0) liveWarmByIndexName.set(name, spec);
16676
+ }
16513
16677
  const recordedOldBilling = previousProperties["BillingMode"];
16514
16678
  const recordedOldBillingAbsent = recordedOldBilling === void 0;
16515
16679
  const recordedOldBillingUsable = typeof recordedOldBilling === "string" && recordedOldBilling.trim() !== "";
@@ -16659,11 +16823,11 @@ var DynamoDBGlobalTableProvider = class {
16659
16823
  const gsi = byIndexName.get(indexName);
16660
16824
  if (!gsi?.ProvisionedThroughput) continue;
16661
16825
  const previousWarm = previousByIndexName.get(indexName)?.WarmThroughput;
16662
- const warmChanged = gsi.WarmThroughput !== void 0 && !deepEqual$1(gsi.WarmThroughput, previousWarm);
16826
+ const warmToSend = gsi.WarmThroughput !== void 0 && !deepEqual$1(gsi.WarmThroughput, previousWarm) ? this.warmThroughputForSend(indexName, gsi.WarmThroughput, liveWarmByIndexName, logicalId, physicalId) : void 0;
16663
16827
  indexUpdates.push({ Update: {
16664
16828
  IndexName: indexName,
16665
16829
  ProvisionedThroughput: gsi.ProvisionedThroughput,
16666
- ...warmChanged && { WarmThroughput: gsi.WarmThroughput }
16830
+ ...warmToSend && { WarmThroughput: warmToSend }
16667
16831
  } });
16668
16832
  if (survivingIndexNames.has(indexName)) gsiHandledByBillingFlip.add(indexName);
16669
16833
  }
@@ -16798,13 +16962,14 @@ var DynamoDBGlobalTableProvider = class {
16798
16962
  }
16799
16963
  for (const gsi of gsiDiff.added) {
16800
16964
  if (!gsi.IndexName || !gsi.KeySchema || !gsi.Projection) continue;
16965
+ const addedWarmToSend = this.warmThroughputForSend(gsi.IndexName, gsi.WarmThroughput, liveWarmByIndexName, logicalId, physicalId);
16801
16966
  const gsiUpdate = { Create: {
16802
16967
  IndexName: gsi.IndexName,
16803
16968
  KeySchema: gsi.KeySchema,
16804
16969
  Projection: gsi.Projection,
16805
16970
  ...gsi.ProvisionedThroughput && { ProvisionedThroughput: gsi.ProvisionedThroughput },
16806
16971
  ...gsi.OnDemandThroughput && { OnDemandThroughput: gsi.OnDemandThroughput },
16807
- ...gsi.WarmThroughput && { WarmThroughput: gsi.WarmThroughput }
16972
+ ...addedWarmToSend && { WarmThroughput: addedWarmToSend }
16808
16973
  } };
16809
16974
  await this.dynamoDBClient.send(new UpdateTableCommand({
16810
16975
  TableName: physicalId,
@@ -16831,9 +16996,14 @@ var DynamoDBGlobalTableProvider = class {
16831
16996
  if (Object.keys(onDemand).length > 0) update.OnDemandThroughput = onDemand;
16832
16997
  if (!billingFlipped && gsi.ProvisionedThroughput) update.ProvisionedThroughput = gsi.ProvisionedThroughput;
16833
16998
  const previousSdk = previousSdkByName.get(gsi.IndexName);
16834
- if (gsi.WarmThroughput !== void 0 && !deepEqual$1(gsi.WarmThroughput, previousSdk?.WarmThroughput)) update.WarmThroughput = gsi.WarmThroughput;
16999
+ let warmDecreaseRefused = false;
17000
+ if (gsi.WarmThroughput !== void 0 && !deepEqual$1(gsi.WarmThroughput, previousSdk?.WarmThroughput)) {
17001
+ const warmToSend = this.warmThroughputForSend(gsi.IndexName, gsi.WarmThroughput, liveWarmByIndexName, logicalId, physicalId);
17002
+ if (warmToSend) update.WarmThroughput = warmToSend;
17003
+ else warmDecreaseRefused = true;
17004
+ }
16835
17005
  if (!update.ProvisionedThroughput && !update.OnDemandThroughput && !update.WarmThroughput) {
16836
- 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.`);
17006
+ 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.`);
16837
17007
  continue;
16838
17008
  }
16839
17009
  const gsiUpdate = { Update: update };
@@ -17310,9 +17480,27 @@ var DynamoDBGlobalTableProvider = class {
17310
17480
  try {
17311
17481
  if (hasTransitionalIndex(preDeleteIndexes)) {
17312
17482
  this.logger.debug(`Waiting for indexes on ${physicalId} to settle before DeleteTable (issue #1521)`);
17313
- await this.waitForIndexesActive(physicalId, logicalId);
17483
+ await this.waitForIndexesActive(physicalId, logicalId, { proceedNote: DELETE_INDEX_WAIT_PROCEED_NOTE });
17314
17484
  }
17315
- await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
17485
+ let deleteAttempts = 0;
17486
+ await withRetry(async () => {
17487
+ if (deleteAttempts++ > 0) {
17488
+ if (deleteAttempts === 2) {
17489
+ const remainingAttempts = 1 + DELETE_INDEX_BUSY_MAX_RETRIES - deleteAttempts;
17490
+ this.logger.warn(`DynamoDB GlobalTable ${logicalId}: AWS refused DeleteTable on ${physicalId} because a global secondary index is still being created, updated or deleted — application auto-scaling can start an index capacity change at any moment, including during this destroy. Waiting for the index to settle and retrying (up to ${remainingAttempts} more attempts, each preceded by up to ${DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS} DescribeTable polls ~1s apart — a little over a minute of settling per attempt). Nothing is wrong with the table; a large index backfill can outlast this budget, in which case the destroy fails with AWS's own message and re-running it succeeds once the index is ACTIVE.`);
17491
+ }
17492
+ await this.waitForIndexesActive(physicalId, logicalId, {
17493
+ maxAttempts: DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS,
17494
+ proceedNote: DELETE_INDEX_WAIT_PROCEED_NOTE
17495
+ });
17496
+ }
17497
+ await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
17498
+ }, logicalId, {
17499
+ maxRetries: DELETE_INDEX_BUSY_MAX_RETRIES,
17500
+ isRetryable: (message) => isIndexBusyDeleteError(message),
17501
+ logger: this.logger,
17502
+ sleep: (ms) => deleteTableRetryDelays.sleep ? deleteTableRetryDelays.sleep(ms) : new Promise((resolve) => setTimeout(resolve, ms))
17503
+ });
17316
17504
  await this.waitForTableGone(physicalId, logicalId);
17317
17505
  this.logger.debug(`Successfully deleted DynamoDB GlobalTable ${logicalId}`);
17318
17506
  } catch (error) {
@@ -17928,22 +18116,49 @@ var DynamoDBGlobalTableProvider = class {
17928
18116
  * throwing. A missed registration self-heals on the next deploy (the
17929
18117
  * reconcile re-asserts anything not already present), whereas throwing
17930
18118
  * here would fail a deploy whose actual resources are all correct. Index
17931
- * backfill on a large table can take a long time, hence the 15-minute cap.
17932
- * The caller that follows it with a mutating call keeps AWS's own error as
17933
- * the backstop for the case where even that cap is not enough.
17934
- */
17935
- async waitForIndexesActive(tableName, logicalId) {
17936
- const maxAttempts = 900;
18119
+ * backfill on a large table can take a long time, hence the 15-minute
18120
+ * DEFAULT cap. The caller that follows it with a mutating call keeps AWS's
18121
+ * own error as the backstop for the case where even that cap is not enough.
18122
+ *
18123
+ * `opts.maxAttempts` exists because the cap is only right for a wait that
18124
+ * runs ONCE: the #1830 delete retry re-runs this per attempt, so the caller's
18125
+ * wall clock is the product and the default would blow past the per-resource
18126
+ * deadline `destroy-runner.ts` enforces.
18127
+ *
18128
+ * `opts.proceedNote` covers the OTHER thing that is caller-specific: what it
18129
+ * costs when the wait ends WITHOUT confirming. Both arms that end that way —
18130
+ * the timeout, and a `DescribeTable` that failed for a reason other than
18131
+ * throttling or a gone table — stop waiting and let the caller proceed, so
18132
+ * one note serves both. The consequences genuinely differ: on the delete
18133
+ * path AWS refuses the `DeleteTable` and that refusal is the backstop, while
18134
+ * the auto-scaling caller has NO backstop — `reconcileAutoScalingTargets`
18135
+ * simply skips an index that is not ready and nothing downstream refuses.
18136
+ * The default sentence is the auto-scaling one; every delete-path caller
18137
+ * passes {@link DELETE_INDEX_WAIT_PROCEED_NOTE}.
18138
+ */
18139
+ async waitForIndexesActive(tableName, logicalId, opts) {
18140
+ const maxAttempts = opts?.maxAttempts ?? 900;
18141
+ const proceedNote = opts?.proceedNote ?? "auto-scaling registration for a still-building index may have been skipped. The next deploy re-asserts it.";
17937
18142
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
17938
18143
  try {
17939
18144
  if (!hasTransitionalIndex((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.GlobalSecondaryIndexes ?? [])) return;
17940
18145
  } catch (err) {
17941
- this.logger.debug(`DescribeTable while waiting for indexes on ${tableName}: ${err instanceof Error ? err.message : String(err)}`);
18146
+ const message = err instanceof Error ? err.message : String(err);
18147
+ if (isThrottlingError(err)) {
18148
+ this.logger.debug(`DescribeTable throttled while waiting for indexes on ${tableName} (attempt ${attempt}/${maxAttempts}); still waiting: ${message}`);
18149
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
18150
+ continue;
18151
+ }
18152
+ if (err instanceof ResourceNotFoundException$1) {
18153
+ this.logger.debug(`Table ${tableName} (${logicalId}) no longer exists while waiting for indexes`);
18154
+ return;
18155
+ }
18156
+ this.logger.warn(`DescribeTable failed while waiting for indexes on ${tableName} (${logicalId}), so cdkd stopped waiting; ${proceedNote} DescribeTable error: ${message}`);
17942
18157
  return;
17943
18158
  }
17944
18159
  await new Promise((resolve) => setTimeout(resolve, 1e3));
17945
18160
  }
17946
- 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.`);
18161
+ this.logger.warn(`Indexes on ${tableName} (${logicalId}) did not all reach ACTIVE within ${maxAttempts} DescribeTable polls (~1s apart, so a little over ${maxAttempts}s of wall clock); ${proceedNote}`);
17947
18162
  }
17948
18163
  /**
17949
18164
  * Wait until a specific replica's `ReplicaStatus` flips to ACTIVE.
@@ -18082,6 +18297,14 @@ function autoScalingResourceId(tableName, indexName) {
18082
18297
  */
18083
18298
  const autoScalingRetryDelays = {};
18084
18299
  /**
18300
+ * Test seam for the index-busy `DeleteTable` retry's backoff (issue #1830),
18301
+ * mirroring {@link autoScalingRetryDelays}. Production leaves `sleep`
18302
+ * undefined so `withRetry`'s real schedule applies; a test injects a no-op so
18303
+ * the ~47s budget does not have to be waited out. Kept SEPARATE from the
18304
+ * auto-scaling seam so a test can slow one path without silencing the other.
18305
+ */
18306
+ const deleteTableRetryDelays = {};
18307
+ /**
18085
18308
  * Resource ids per `DescribeScalableTargets` request. Kept at the API's
18086
18309
  * documented per-page ceiling rather than a larger guess — the request is
18087
18310
  * paginated anyway, and over-asking risks a `ValidationException` that would
@@ -18235,12 +18458,113 @@ const INDEX_TRANSITIONAL_STATUSES = /* @__PURE__ */ new Set([
18235
18458
  function hasTransitionalIndex(indexes) {
18236
18459
  return (indexes ?? []).some((gsi) => INDEX_TRANSITIONAL_STATUSES.has(gsi.IndexStatus ?? ""));
18237
18460
  }
18238
- /** Coerce a CFn numeric (CFn is stringly-typed) to a finite number. */
18239
- function toFiniteNumber(value) {
18240
- if (value === void 0 || value === null || value === "") return void 0;
18241
- if (typeof value !== "number" && typeof value !== "string") return void 0;
18242
- const n = Number(value);
18243
- return Number.isFinite(n) ? n : void 0;
18461
+ /**
18462
+ * AWS's refusal when `DeleteTable` lands while one of the table's GSIs is
18463
+ * mid-transition (issue #1830). Observed verbatim on a real destroy:
18464
+ *
18465
+ * Attempt to change a resource which is still in use: Cannot delete table
18466
+ * while indexes are being created, updated, or deleted.
18467
+ *
18468
+ * Matched on the leading clause rather than on the whole sentence: the
18469
+ * trailing status list is AWS wording that can gain a state without changing
18470
+ * what the message MEANS, and the leading clause is already specific enough
18471
+ * that nothing else in the DynamoDB surface produces it. Case-insensitive
18472
+ * because the text arrives inside a wrapper prefix whose casing is not ours.
18473
+ *
18474
+ * Deliberately NOT keyed on the exception NAME: AWS reports this as a plain
18475
+ * `ResourceInUseException`, which is the same name it uses for genuinely
18476
+ * terminal conflicts (deleting a table that is `CREATING`, creating one that
18477
+ * already exists). Retrying on the name would turn those into a 47s stall
18478
+ * before the same failure.
18479
+ */
18480
+ const INDEX_BUSY_DELETE_MESSAGE = /cannot delete table while indexes are being/i;
18481
+ function isIndexBusyDeleteError(message) {
18482
+ return INDEX_BUSY_DELETE_MESSAGE.test(message);
18483
+ }
18484
+ /**
18485
+ * Retry budget for the index-busy `DeleteTable` refusal (issue #1830).
18486
+ *
18487
+ * Left at `withRetry`'s own default of 8 rather than raised. Named rather than
18488
+ * inlined because passing it at all is what opts this call out of
18489
+ * `withRetry`'s dense IAM-propagation schedule, which would be wrong for a
18490
+ * condition measured in seconds-to-minutes rather than sub-second.
18491
+ *
18492
+ * The count MULTIPLIES the per-attempt re-arm poll below, so the two have to
18493
+ * be read together — see {@link DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS} for the
18494
+ * arithmetic and for why the poll is not the 15-minute one.
18495
+ */
18496
+ const DELETE_INDEX_BUSY_MAX_RETRIES = 8;
18497
+ /**
18498
+ * Seconds the index-busy `DeleteTable` retry spends re-arming BEFORE each
18499
+ * retry (issue #1830) — one `DescribeTable` per second, and it returns on the
18500
+ * first one that reports every index settled.
18501
+ *
18502
+ * Bounded well under
18503
+ * {@link DynamoDBGlobalTableProvider['waitForIndexesActive']}'s 15-minute
18504
+ * default because THAT default is sized for a wait that runs once, while this
18505
+ * one runs per retry: the caller's wall clock is
18506
+ * `DELETE_INDEX_BUSY_MAX_RETRIES x this + withRetry's ~47s of backoff`, and
18507
+ * `destroy-runner.ts` caps a single `delete()` at the per-resource deadline
18508
+ * (30 min by default; this provider declares no `getMinResourceTimeoutMs` to
18509
+ * lift it). At 900s the product was ~2h, so a genuinely stuck index produced a
18510
+ * 30-minute wait ending in a generic `ResourceTimeoutError` that never
18511
+ * mentions indexes. At 60s the worst case is ~8.8 min and the user gets AWS's
18512
+ * own actionable sentence instead.
18513
+ *
18514
+ * The FIRST attempt is unaffected — it does not re-arm at all, and the #1521
18515
+ * pre-delete gate ahead of the loop keeps its full 15-minute poll.
18516
+ */
18517
+ const DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS = 60;
18518
+ /**
18519
+ * What proceeding costs when a DELETE-path index-settle wait ends WITHOUT
18520
+ * confirming — passed as
18521
+ * {@link DynamoDBGlobalTableProvider['waitForIndexesActive']}'s `proceedNote`
18522
+ * by BOTH delete-path callers (the #1521 pre-delete gate and the #1830 retry
18523
+ * re-arm).
18524
+ *
18525
+ * The default note that helper falls back to is the auto-scaling caller's, and
18526
+ * it is not merely unhelpful here but wrong: nothing on the delete path
18527
+ * re-asserts anything on a later deploy. Conversely the delete path really
18528
+ * does have a backstop — `DeleteTable` follows immediately and AWS refuses it
18529
+ * again if the index is still transitioning — which the auto-scaling caller
18530
+ * does not: `reconcileAutoScalingTargets` skips an index that is not ready and
18531
+ * nothing downstream notices. One sentence per caller, so neither claims the
18532
+ * other's safety net.
18533
+ */
18534
+ const DELETE_INDEX_WAIT_PROCEED_NOTE = `the DeleteTable goes ahead anyway and AWS's own refusal stays the backstop.`;
18535
+ /**
18536
+ * Build the {@link ThroughputDiagnostic} for a `WarmThroughput` block that did
18537
+ * not survive coercion intact, or `undefined` when it did.
18538
+ *
18539
+ * Two distinct outcomes, worded differently because the remedy differs:
18540
+ * - PARTIAL — one member dropped, the other still sent. The call goes ahead;
18541
+ * the user needs to know which half silently did not.
18542
+ * - REFUSED — nothing sendable. No call is made for the property at all,
18543
+ * which is the point (issue #1857 defect 3): forwarding the malformed block
18544
+ * would surface as an AWS validation error that names neither cdkd nor the
18545
+ * property.
18546
+ */
18547
+ function warmThroughputDiagnostic(raw, coercion, indexName) {
18548
+ if (raw === void 0 || raw === null) return void 0;
18549
+ const scope = {
18550
+ kind: "unresolved-member",
18551
+ member: "WarmThroughput"
18552
+ };
18553
+ const dropped = coercion.droppedMembers.join(" / ");
18554
+ if (coercion.spec) {
18555
+ if (coercion.droppedMembers.length === 0) return void 0;
18556
+ const kept = WARM_THROUGHPUT_MEMBERS.filter((m) => coercion.spec?.[m] !== void 0).join(" / ");
18557
+ return {
18558
+ ...scope,
18559
+ ...indexName !== void 0 && { indexName },
18560
+ message: `WarmThroughput.${dropped} did not resolve to a number and was DROPPED; ${kept} is still sent. AWS types these as numbers, so a quoted "12000" is fine but an unresolved intrinsic or an object is not. The dropped member keeps its current AWS value until the template supplies a usable one.`
18561
+ };
18562
+ }
18563
+ return {
18564
+ ...scope,
18565
+ ...indexName !== void 0 && { indexName },
18566
+ message: `WarmThroughput is declared but no member resolved to a number${dropped ? ` (${dropped})` : ""}, so the whole block is REFUSED and not sent. Forwarding it would fail as an opaque AWS validation error naming neither cdkd nor the property. Set WarmThroughput.ReadUnitsPerSecond / WriteUnitsPerSecond to numbers, or remove the block. Nothing else about the index is affected.`
18567
+ };
18244
18568
  }
18245
18569
  /**
18246
18570
  * Merge an explicitly-supplied, already-SDK-shaped throughput block over a
@@ -18535,7 +18859,10 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "
18535
18859
  KeySchema: gsi["KeySchema"],
18536
18860
  Projection: gsi["Projection"]
18537
18861
  };
18538
- if (gsi["WarmThroughput"] !== void 0) sdk.WarmThroughput = gsi["WarmThroughput"];
18862
+ const warmCoercion = coerceWarmThroughput$1(gsi["WarmThroughput"]);
18863
+ if (warmCoercion.spec) sdk.WarmThroughput = warmCoercion.spec;
18864
+ const warmDiagnostic = warmThroughputDiagnostic(gsi["WarmThroughput"], warmCoercion, typeof indexName === "string" ? indexName : void 0);
18865
+ if (warmDiagnostic) diagnostics?.push(warmDiagnostic);
18539
18866
  const explicitProvisioned = asRecord$1(gsi["ProvisionedThroughput"]);
18540
18867
  const explicitOnDemand = asRecord$1(gsi["OnDemandThroughput"]);
18541
18868
  if (explicitProvisioned && billingMode !== "PROVISIONED") diagnostics?.push({
@@ -79780,7 +80107,7 @@ function createMigrateCommand() {
79780
80107
  */
79781
80108
  function buildProgram() {
79782
80109
  const program = new Command();
79783
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.27");
80110
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.28");
79784
80111
  program.hook("preAction", (_thisCommand, actionCommand) => {
79785
80112
  const { profile } = actionCommand.optsWithGlobals();
79786
80113
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;