@go-to-k/cdkd 0.283.28 → 0.283.30

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-5rA6lMn8.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-Bb4ECtz3.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-DzaowTd_.js";
4
+ import { t as ASGProvider } from "./asg-provider-BpVWOeU3.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";
@@ -14192,6 +14192,274 @@ function isWarmThroughputDecrease(desired, live) {
14192
14192
  return sawDecrease;
14193
14193
  }
14194
14194
 
14195
+ //#endregion
14196
+ //#region src/provisioning/dynamodb-index-busy-delete.ts
14197
+ /**
14198
+ * The index-busy `DeleteTable` rule that `AWS::DynamoDB::Table` and
14199
+ * `AWS::DynamoDB::GlobalTable` both need, in ONE spelling.
14200
+ *
14201
+ * AWS refuses a `DeleteTable` while any of the table's global secondary
14202
+ * indexes is mid-transition:
14203
+ *
14204
+ * Attempt to change a resource which is still in use: Cannot delete table
14205
+ * while indexes are being created, updated, or deleted.
14206
+ *
14207
+ * The condition is TRANSIENT and self-resolving — a real
14208
+ * `dynamodb-globaltable` destroy (us-east-1, 2026-08-13) lost one table to it
14209
+ * and the very next `cdkd destroy` succeeded with no other change — and
14210
+ * application auto-scaling can start an index capacity change at any moment,
14211
+ * so any table with an autoscaled GSI can hit it. Surfacing it as a hard
14212
+ * `PartialFailureError` with state preserved makes the user re-run a destroy
14213
+ * for something that clears itself in seconds.
14214
+ *
14215
+ * **Provenance, stated precisely.** The rule shipped for
14216
+ * `AWS::DynamoDB::GlobalTable` first (issue #1830, PR #1930), provider-locally
14217
+ * — deliberately, since `src/deployment/retryable-errors.ts` was owned by
14218
+ * another lane at the time. Issue #1931 is the sibling `AWS::DynamoDB::Table`
14219
+ * gap that PR left: its `delete()` had no retry at all. Rather than write the
14220
+ * classifier, the budget and the warning a second time, they were LIFTED here
14221
+ * and both providers now read them.
14222
+ *
14223
+ * What did NOT change for `GlobalTable` is its AWS-FACING behaviour: the call
14224
+ * order, the budgets, the #1521 pre-delete gate and the replica teardown are
14225
+ * identical. Two LOG LINES did change, deliberately, and the claim is scoped
14226
+ * rather than blanket because a blanket one is falsifiable by the diff: the
14227
+ * unclassified-error arm now names the error CLASS at warn and keeps AWS's raw
14228
+ * text at debug (it leaked an account id and an assumed-role ARN at default
14229
+ * verbosity), and {@link indexBusyRetryWarning} was reworded so its own
14230
+ * arithmetic reads consistently. That provider's suite was updated to match.
14231
+ *
14232
+ * Living here rather than in either provider is the point: the classifier is a
14233
+ * single regex whose PRECISION is the whole safety argument (see
14234
+ * {@link INDEX_BUSY_DELETE_MESSAGE}), and the retry budget only makes sense
14235
+ * read together with the re-arm poll it multiplies. Two files spelling either
14236
+ * independently is two chances for a later "fix" to move one of them while the
14237
+ * sibling type silently keeps the old answer. Same class as
14238
+ * `dynamodb-warm-throughput.ts`, and the same reason.
14239
+ *
14240
+ * Deliberately NOT here: what only one provider has. `GlobalTable` keeps its
14241
+ * pre-delete #1521 gate (a 15-minute settle wait ahead of the retry loop, sized
14242
+ * for a wait that runs once) and its replica / auto-scaling teardown; the
14243
+ * `Table` provider has neither. What IS shared is everything a second
14244
+ * implementation would otherwise re-derive: the predicate, the classifier, the
14245
+ * two budgets, the proceed note, the warning text and the retry loop itself.
14246
+ *
14247
+ * Issues: #1830 / #1521 (GlobalTable, PR #1930), #1931 (Table).
14248
+ */
14249
+ /**
14250
+ * The GSI states during which AWS refuses another `UpdateTable` or a
14251
+ * `DeleteTable` on the table (issue #1521) — a table is ACTIVE while one of
14252
+ * its indexes is still transitioning, which is what made the serialized
14253
+ * update sequence race and what left a table ORPHANED when the follow-up
14254
+ * destroy hit the same rule.
14255
+ *
14256
+ * `DELETING` is included because a dropped index keeps reporting until it is
14257
+ * actually gone. The test is on the TRANSITIONAL values rather than on
14258
+ * `!== 'ACTIVE'`: an absent or unrecognized status must not park the caller
14259
+ * for its whole cap on a table that is fine.
14260
+ */
14261
+ const INDEX_TRANSITIONAL_STATUSES = /* @__PURE__ */ new Set([
14262
+ "CREATING",
14263
+ "UPDATING",
14264
+ "DELETING"
14265
+ ]);
14266
+ function hasTransitionalIndex(indexes) {
14267
+ return (indexes ?? []).some((gsi) => INDEX_TRANSITIONAL_STATUSES.has(gsi.IndexStatus ?? ""));
14268
+ }
14269
+ /**
14270
+ * AWS's refusal when `DeleteTable` lands while one of the table's GSIs is
14271
+ * mid-transition (issue #1830). Observed verbatim on a real destroy:
14272
+ *
14273
+ * Attempt to change a resource which is still in use: Cannot delete table
14274
+ * while indexes are being created, updated, or deleted.
14275
+ *
14276
+ * Matched on the leading clause rather than on the whole sentence: the
14277
+ * trailing status list is AWS wording that can gain a state without changing
14278
+ * what the message MEANS, and the leading clause is already specific enough
14279
+ * that nothing else in the DynamoDB surface produces it. Case-insensitive
14280
+ * because the text arrives inside a wrapper prefix whose casing is not ours.
14281
+ *
14282
+ * Deliberately NOT keyed on the exception NAME: AWS reports this as a plain
14283
+ * `ResourceInUseException`, which is the same name it uses for genuinely
14284
+ * terminal conflicts (deleting a table that is `CREATING`, creating one that
14285
+ * already exists). Retrying on the name would turn those into a 47s stall
14286
+ * before the same failure.
14287
+ */
14288
+ const INDEX_BUSY_DELETE_MESSAGE = /cannot delete table while indexes are being/i;
14289
+ function isIndexBusyDeleteError(message) {
14290
+ return INDEX_BUSY_DELETE_MESSAGE.test(message);
14291
+ }
14292
+ /**
14293
+ * Retry budget for the index-busy `DeleteTable` refusal (issue #1830).
14294
+ *
14295
+ * Left at `withRetry`'s own default of 8 rather than raised. Named rather than
14296
+ * inlined because passing it at all is what opts this call out of
14297
+ * `withRetry`'s dense IAM-propagation schedule, which would be wrong for a
14298
+ * condition measured in seconds-to-minutes rather than sub-second.
14299
+ *
14300
+ * The count MULTIPLIES the per-attempt re-arm poll below, so the two have to
14301
+ * be read together — see {@link DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS} for the
14302
+ * arithmetic and for why the poll is not a 15-minute one.
14303
+ */
14304
+ const DELETE_INDEX_BUSY_MAX_RETRIES = 8;
14305
+ /**
14306
+ * `DescribeTable` polls (~1s apart) the index-busy `DeleteTable` retry spends
14307
+ * re-arming BEFORE each retry (issue #1830) — it returns on the first one that
14308
+ * reports every index settled.
14309
+ *
14310
+ * Bounded well under the 15-minute default `GlobalTable`'s pre-delete #1521
14311
+ * gate uses, because THAT default is sized for a wait that runs once while
14312
+ * this one runs per retry: the caller's wall clock is
14313
+ * `DELETE_INDEX_BUSY_MAX_RETRIES x this + withRetry's ~47s of backoff`, and
14314
+ * `destroy-runner.ts` runs the delete under a per-resource deadline (30 min by
14315
+ * default; neither DynamoDB provider declares a `getMinResourceTimeoutMs` to
14316
+ * lift it). At 900 polls the product was ~2h, so a genuinely stuck index
14317
+ * produced a 30-minute wait ending in a generic `ResourceTimeoutError` that
14318
+ * never mentions indexes. At 60 the LOOP's worst case is ~8.8 min and the user
14319
+ * gets AWS's own actionable sentence instead.
14320
+ *
14321
+ * **What that deadline actually wraps**, since the arithmetic depends on it:
14322
+ * NOT one `delete()` but `destroy-runner.ts`'s whole outer retry loop, which
14323
+ * calls `delete()` up to 4 times (`maxAttempts = 3`) inside the one deadline.
14324
+ * Four times ~8.8 min would blow straight through 30 min. It does not, and the
14325
+ * reason is load-bearing rather than incidental: that outer loop only retries
14326
+ * when `isRetryableTransientError` says so, and this refusal matches no entry
14327
+ * in `RETRYABLE_ERROR_MESSAGE_PATTERNS` (verified against the real classifier —
14328
+ * both AWS's raw sentence and the `Failed to delete DynamoDB table ...` wrap
14329
+ * return false), so the outer loop runs `delete()` exactly ONCE for it and the
14330
+ * budget below is not re-multiplied. Adding this message to those patterns
14331
+ * would silently make the worst case ~35 min — over the deadline.
14332
+ *
14333
+ * The FIRST attempt is unaffected — it does not re-arm at all.
14334
+ */
14335
+ const DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS = 60;
14336
+ /**
14337
+ * What proceeding costs when a DELETE-path index-settle wait ends WITHOUT
14338
+ * confirming — passed as {@link waitForIndexesSettled}'s `proceedNote` by every
14339
+ * delete-path caller (`GlobalTable`'s #1521 pre-delete gate, and the #1830 /
14340
+ * #1931 retry re-arm on both providers).
14341
+ *
14342
+ * The note is caller-supplied because the consequence genuinely differs: the
14343
+ * delete path has a backstop — `DeleteTable` follows immediately and AWS
14344
+ * refuses it again if the index is still transitioning — while
14345
+ * `GlobalTable`'s auto-scaling caller has none (`reconcileAutoScalingTargets`
14346
+ * skips an index that is not ready and nothing downstream notices). One
14347
+ * sentence per caller, so neither claims the other's safety net.
14348
+ */
14349
+ const DELETE_INDEX_WAIT_PROCEED_NOTE = `the DeleteTable goes ahead anyway and AWS's own refusal stays the backstop.`;
14350
+ /**
14351
+ * Poll `DescribeTable` until no GSI is transitioning, bounded by
14352
+ * `maxAttempts` (~1s apart).
14353
+ *
14354
+ * **Best-effort by design**: on timeout it warns and returns rather than
14355
+ * throwing. Index backfill on a large table can take a long time, and a throw
14356
+ * here would fail a caller whose actual resources are all correct — on the
14357
+ * delete path it would STRAND the resource, which is the failure this whole
14358
+ * module exists to remove.
14359
+ *
14360
+ * `maxAttempts` and `proceedNote` are required rather than defaulted: the two
14361
+ * callers differ on both (a wait that runs ONCE versus one that runs per retry
14362
+ * and multiplies; a caller with a backstop versus one without), and a default
14363
+ * would silently give one caller the other's answer.
14364
+ */
14365
+ async function waitForIndexesSettled(opts) {
14366
+ const { tableName, logicalId, logger, describeTable, maxAttempts, proceedNote } = opts;
14367
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
14368
+ try {
14369
+ if (!hasTransitionalIndex((await describeTable()).Table?.GlobalSecondaryIndexes ?? [])) return;
14370
+ } catch (err) {
14371
+ const message = err instanceof Error ? err.message : String(err);
14372
+ if (isThrottlingError(err)) {
14373
+ logger.debug(`DescribeTable throttled while waiting for indexes on ${tableName} (attempt ${attempt}/${maxAttempts}); still waiting: ${message}`);
14374
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
14375
+ continue;
14376
+ }
14377
+ if (err instanceof ResourceNotFoundException$1) {
14378
+ logger.debug(`Table ${tableName} (${logicalId}) no longer exists while waiting for indexes`);
14379
+ return;
14380
+ }
14381
+ const errorName = err instanceof Error ? err.name : typeof err;
14382
+ logger.debug(`DescribeTable failed while waiting for indexes on ${tableName} (${logicalId}): ${message}`);
14383
+ logger.warn(`DescribeTable failed (${errorName}) while waiting for indexes on ${tableName} (${logicalId}), so cdkd stopped waiting; ${proceedNote} Re-run with --verbose for AWS's own message.`);
14384
+ return;
14385
+ }
14386
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
14387
+ }
14388
+ logger.warn(`Indexes on ${tableName} (${logicalId}) did not all reach ACTIVE within ${maxAttempts} DescribeTable polls (~1s apart, so a little over ${maxAttempts}s of wall clock); ${proceedNote}`);
14389
+ }
14390
+ /**
14391
+ * The ONE line the retry prints at default verbosity, for both providers.
14392
+ *
14393
+ * `withRetry` announces its retries through `opts.logger?.debug`, so without
14394
+ * this a delete spending minutes re-polling printed nothing naming the cause —
14395
+ * and the bounded budget above can end in a timeout that has to be diagnosable
14396
+ * from an ordinary run's output.
14397
+ *
14398
+ * `attemptNumber` is the attempt now STARTING (2 for the first retry). The
14399
+ * remaining-attempt count is DERIVED from it and never spelled out: `withRetry`
14400
+ * runs `1 + maxRetries` attempts in total, and writing the number by hand is
14401
+ * how this line came to promise 8 further attempts when 7 were left.
14402
+ *
14403
+ * The per-attempt settle budget is attached to "this attempt plus the N more"
14404
+ * rather than to the N alone, because the re-arm runs before the attempt this
14405
+ * very line announces too — 8 re-arms for 7 remaining attempts. Saying "N more
14406
+ * attempts, each preceded by ..." made one sentence quote two different counts.
14407
+ *
14408
+ * It must NOT carry AWS's own sentence: the `dynamodb-globaltable` integ greps
14409
+ * the destroy log for `Cannot delete table while indexes are being` to prove
14410
+ * AWS really refused, and a cdkd-authored copy would satisfy that grep without
14411
+ * the refusal having happened.
14412
+ */
14413
+ function indexBusyRetryWarning(opts) {
14414
+ const remainingAttempts = 9 - opts.attemptNumber;
14415
+ return `DynamoDB ${opts.typeLabel} ${opts.logicalId}: AWS refused DeleteTable on ${opts.physicalId} because a global secondary index is still being created, updated or deleted — application auto-scaling can start an index capacity change at any moment, including during this destroy. Waiting for the index to settle and retrying (this attempt plus up to ${remainingAttempts} more attempts; each one first waits up to ${60} DescribeTable polls ~1s apart — a little over a minute of settling — before its DeleteTable). Nothing is wrong with the table; a large index backfill can outlast this budget, in which case the destroy fails with AWS's own message and re-running it succeeds once the index is ACTIVE.`;
14416
+ }
14417
+ /**
14418
+ * Issue the caller's `DeleteTable`, retrying ONLY the transient index-busy
14419
+ * refusal and re-arming on the CONDITION between attempts.
14420
+ *
14421
+ * Retried here rather than by widening the shared
14422
+ * `RETRYABLE_ERROR_MESSAGE_PATTERNS`: the same `ResourceInUseException` text
14423
+ * also surfaces on CREATE / UPDATE paths where it is NOT self-clearing, and a
14424
+ * `DeleteTable` is the one call site that can legitimately wait it out. Same
14425
+ * scoping rationale as `isNameCooldownError` / `isRecreateRetryableError`.
14426
+ *
14427
+ * `reArm` runs BEFORE every retry and never before the first attempt. It is the
14428
+ * caller's because the poll needs the caller's client, but every caller is
14429
+ * expected to bound it with {@link DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS}: an
14430
+ * index backfill outlasts any fixed backoff grid, while a settle poll returns
14431
+ * on its first `DescribeTable` once the index is ACTIVE.
14432
+ *
14433
+ * `sleepSeam` is read per SLEEP rather than once when the options bag is built,
14434
+ * so a test that installs the no-op from INSIDE the first refusal — i.e. after
14435
+ * `delete()` has already built the bag — still gets it. A value captured at
14436
+ * construction time would be `undefined` there and would silently pay
14437
+ * `withRetry`'s real ~47s schedule. Passing `sleep` unconditionally is safe:
14438
+ * `withRetry`'s dense-schedule detection keys on `maxRetries` /
14439
+ * `initialDelayMs` / `maxDelayMs` / `isRetryable`, all of which this caller
14440
+ * already sets.
14441
+ */
14442
+ async function deleteTableWithIndexBusyRetry(opts) {
14443
+ let deleteAttempts = 0;
14444
+ await withRetry(async () => {
14445
+ if (deleteAttempts++ > 0) {
14446
+ if (deleteAttempts === 2) opts.logger.warn(indexBusyRetryWarning({
14447
+ typeLabel: opts.typeLabel,
14448
+ logicalId: opts.logicalId,
14449
+ physicalId: opts.physicalId,
14450
+ attemptNumber: deleteAttempts
14451
+ }));
14452
+ await opts.reArm();
14453
+ }
14454
+ await opts.deleteTable();
14455
+ }, opts.logicalId, {
14456
+ maxRetries: 8,
14457
+ isRetryable: (message) => isIndexBusyDeleteError(message),
14458
+ logger: opts.logger,
14459
+ sleep: (ms) => opts.sleepSeam.sleep ? opts.sleepSeam.sleep(ms) : new Promise((resolve) => setTimeout(resolve, ms))
14460
+ });
14461
+ }
14462
+
14195
14463
  //#endregion
14196
14464
  //#region src/provisioning/providers/dynamodb-table-provider.ts
14197
14465
  /**
@@ -14720,6 +14988,18 @@ function desiredIndexEntriesByName(value) {
14720
14988
  }
14721
14989
  return byName;
14722
14990
  }
14991
+ /**
14992
+ * Test seam for the index-busy `DeleteTable` retry's backoff (issue #1931),
14993
+ * mirroring the sibling provider's export of the same name. Production leaves
14994
+ * `sleep` undefined so `withRetry`'s real schedule applies; a test injects a
14995
+ * no-op so the ~47s budget does not have to be waited out.
14996
+ *
14997
+ * One seam PER PROVIDER rather than one on the shared module: a `Table` test
14998
+ * that silences this backoff must not also silence the `GlobalTable` one, since
14999
+ * the two suites run in the same worker and a suite that stopped paying a delay
15000
+ * it meant to exercise fails silently — it just gets faster.
15001
+ */
15002
+ const deleteTableRetryDelays$1 = {};
14723
15003
  var DynamoDBTableProvider = class {
14724
15004
  dynamoDBClient;
14725
15005
  logger = getLogger().child("DynamoDBTableProvider");
@@ -15054,7 +15334,24 @@ var DynamoDBTableProvider = class {
15054
15334
  if (!(flipError instanceof ResourceNotFoundException$1)) this.logger.debug(`Could not disable DeletionProtectionEnabled on ${physicalId}: ${flipError instanceof Error ? flipError.message : String(flipError)}`);
15055
15335
  }
15056
15336
  try {
15057
- await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
15337
+ await deleteTableWithIndexBusyRetry({
15338
+ logicalId,
15339
+ physicalId,
15340
+ typeLabel: "table",
15341
+ logger: this.logger,
15342
+ deleteTable: async () => {
15343
+ await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
15344
+ },
15345
+ reArm: () => waitForIndexesSettled({
15346
+ tableName: physicalId,
15347
+ logicalId,
15348
+ logger: this.logger,
15349
+ describeTable: () => this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId })),
15350
+ maxAttempts: 60,
15351
+ proceedNote: DELETE_INDEX_WAIT_PROCEED_NOTE
15352
+ }),
15353
+ sleepSeam: deleteTableRetryDelays$1
15354
+ });
15058
15355
  this.logger.debug(`Successfully deleted DynamoDB table ${logicalId}`);
15059
15356
  } catch (error) {
15060
15357
  if (error instanceof ResourceNotFoundException$1) {
@@ -17482,24 +17779,19 @@ var DynamoDBGlobalTableProvider = class {
17482
17779
  this.logger.debug(`Waiting for indexes on ${physicalId} to settle before DeleteTable (issue #1521)`);
17483
17780
  await this.waitForIndexesActive(physicalId, logicalId, { proceedNote: DELETE_INDEX_WAIT_PROCEED_NOTE });
17484
17781
  }
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),
17782
+ await deleteTableWithIndexBusyRetry({
17783
+ logicalId,
17784
+ physicalId,
17785
+ typeLabel: "GlobalTable",
17501
17786
  logger: this.logger,
17502
- sleep: (ms) => deleteTableRetryDelays.sleep ? deleteTableRetryDelays.sleep(ms) : new Promise((resolve) => setTimeout(resolve, ms))
17787
+ deleteTable: async () => {
17788
+ await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
17789
+ },
17790
+ reArm: () => this.waitForIndexesActive(physicalId, logicalId, {
17791
+ maxAttempts: 60,
17792
+ proceedNote: DELETE_INDEX_WAIT_PROCEED_NOTE
17793
+ }),
17794
+ sleepSeam: deleteTableRetryDelays
17503
17795
  });
17504
17796
  await this.waitForTableGone(physicalId, logicalId);
17505
17797
  this.logger.debug(`Successfully deleted DynamoDB GlobalTable ${logicalId}`);
@@ -18137,28 +18429,14 @@ var DynamoDBGlobalTableProvider = class {
18137
18429
  * passes {@link DELETE_INDEX_WAIT_PROCEED_NOTE}.
18138
18430
  */
18139
18431
  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.";
18142
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
18143
- try {
18144
- if (!hasTransitionalIndex((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.GlobalSecondaryIndexes ?? [])) return;
18145
- } catch (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}`);
18157
- return;
18158
- }
18159
- await new Promise((resolve) => setTimeout(resolve, 1e3));
18160
- }
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}`);
18432
+ await waitForIndexesSettled({
18433
+ tableName,
18434
+ logicalId,
18435
+ logger: this.logger,
18436
+ describeTable: () => this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName })),
18437
+ maxAttempts: opts?.maxAttempts ?? 900,
18438
+ proceedNote: opts?.proceedNote ?? "auto-scaling registration for a still-building index may have been skipped. The next deploy re-asserts it."
18439
+ });
18162
18440
  }
18163
18441
  /**
18164
18442
  * Wait until a specific replica's `ReplicaStatus` flips to ACTIVE.
@@ -18439,100 +18717,6 @@ function asRecord$1(value) {
18439
18717
  */
18440
18718
  const ON_DEMAND_LIMIT_RESET = -1;
18441
18719
  /**
18442
- * The GSI states during which AWS refuses another `UpdateTable` or a
18443
- * `DeleteTable` on the table (issue #1521) — a table is ACTIVE while one of
18444
- * its indexes is still transitioning, which is what made the serialized
18445
- * update sequence race and what left a table ORPHANED when the follow-up
18446
- * destroy hit the same rule.
18447
- *
18448
- * `DELETING` is included because a dropped index keeps reporting until it is
18449
- * actually gone. The test is on the TRANSITIONAL values rather than on
18450
- * `!== 'ACTIVE'`: an absent or unrecognized status must not park the caller
18451
- * for its whole cap on a table that is fine.
18452
- */
18453
- const INDEX_TRANSITIONAL_STATUSES = /* @__PURE__ */ new Set([
18454
- "CREATING",
18455
- "UPDATING",
18456
- "DELETING"
18457
- ]);
18458
- function hasTransitionalIndex(indexes) {
18459
- return (indexes ?? []).some((gsi) => INDEX_TRANSITIONAL_STATUSES.has(gsi.IndexStatus ?? ""));
18460
- }
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
18720
  * Build the {@link ThroughputDiagnostic} for a `WarmThroughput` block that did
18537
18721
  * not survive coercion intact, or `undefined` when it did.
18538
18722
  *
@@ -38659,6 +38843,56 @@ const MFA_FACTOR_SMS = "SMS_MFA";
38659
38843
  const MFA_FACTOR_SOFTWARE_TOKEN = "SOFTWARE_TOKEN_MFA";
38660
38844
  const MFA_FACTOR_EMAIL_OTP = "EMAIL_OTP";
38661
38845
  /**
38846
+ * Read `EnabledMfas` as the pair (recognized list, "the template asked for a
38847
+ * factor at all").
38848
+ *
38849
+ * The second half exists because `Array.isArray` is not the same question as
38850
+ * "did the user declare a factor". A hand-written YAML scalar
38851
+ * (`EnabledMfas: SOFTWARE_TOKEN_MFA`) or an intrinsic that resolves to a String
38852
+ * parameter is a factor DECLARATION that happens to be mis-shaped. Treating it
38853
+ * as absence lets the MfaConfiguration default resolve to `OFF`, which AWS
38854
+ * ACCEPTS — so the pool ships with MFA disabled and the declared factor
38855
+ * dropped, silently. Treating it as intent keeps the default at `OPTIONAL`
38856
+ * instead, which AWS refuses loudly — unless the template pinned
38857
+ * `MfaConfiguration: OFF`, or another factor is enabled. The warning in
38858
+ * `buildMfaConfigRequest` reports which of those three actually happened.
38859
+ * Same reasoning as the unrecognized-spelling case there; this is that hole
38860
+ * reached through the SHAPE instead of the spelling.
38861
+ */
38862
+ function readEnabledMfas(properties) {
38863
+ const raw = properties["EnabledMfas"];
38864
+ if (Array.isArray(raw)) {
38865
+ const factors = raw;
38866
+ return {
38867
+ factors,
38868
+ declaresFactor: factors.length > 0,
38869
+ malformed: false
38870
+ };
38871
+ }
38872
+ const malformed = raw !== void 0 && raw !== null && raw !== "";
38873
+ return {
38874
+ factors: void 0,
38875
+ declaresFactor: malformed,
38876
+ malformed
38877
+ };
38878
+ }
38879
+ /**
38880
+ * True when any MFA-config-API-routed property is present, i.e. a
38881
+ * `SetUserPoolMfaConfig` call will run post-create. When true, `create()` must
38882
+ * NOT forward `MfaConfiguration` to `CreateUserPool`: AWS rejects
38883
+ * `CreateUserPool` with `MfaConfiguration: ON/OPTIONAL` unless the pool already
38884
+ * has SMS configured (+ phone_number auto-verification) OR software-token MFA
38885
+ * enabled — but software-token / email-OTP MFA can only be enabled via the
38886
+ * post-create `SetUserPoolMfaConfig` call, not on `CreateUserPool`. So the
38887
+ * correct sequence is: `CreateUserPool` WITHOUT `MfaConfiguration` (defaults
38888
+ * OFF) -> `SetUserPoolMfaConfig` sets `MfaConfiguration` + the factor blocks
38889
+ * together (the factor satisfies the MFA requirement, no SMS needed). This
38890
+ * mirrors how CloudFormation/CDK sequence the two calls.
38891
+ */
38892
+ function hasMfaConfigProps(properties) {
38893
+ return readEnabledMfas(properties).declaresFactor || !!properties["EmailAuthenticationMessage"] || !!properties["EmailAuthenticationSubject"] || !!properties["WebAuthnRelyingPartyID"] || !!properties["WebAuthnUserVerification"];
38894
+ }
38895
+ /**
38662
38896
  * Build the `SetUserPoolMfaConfig` request from the CFn-level MFA properties,
38663
38897
  * or return `undefined` when none of the MFA-config-API-routed properties are
38664
38898
  * present (so the caller skips the extra control-plane call entirely).
@@ -38674,37 +38908,21 @@ const MFA_FACTOR_EMAIL_OTP = "EMAIL_OTP";
38674
38908
  * omitted `MfaConfiguration` defaults to OFF on the wire — which resets the
38675
38909
  * pool to MFA-disabled and makes AWS reject (or silently drop) the per-factor
38676
38910
  * sub-blocks we are trying to enable. Use the template's `MfaConfiguration`
38677
- * when present; default to `OPTIONAL` when factors are present but the template
38678
- * omitted it (factors are meaningless under OFF, and OPTIONAL enables them
38679
- * without forcing MFA on every user).
38911
+ * when present; when the template omitted it, default by whether this call
38912
+ * enables an MFA FACTOR — `OPTIONAL` if it does, `OFF` (CloudFormation's own
38913
+ * default) if it does not. The body comment on that decision explains why both
38914
+ * halves of the factor test are load-bearing.
38680
38915
  */
38681
- /**
38682
- * True when any MFA-config-API-routed property is present, i.e. a
38683
- * `SetUserPoolMfaConfig` call will run post-create. When true, `create()` must
38684
- * NOT forward `MfaConfiguration` to `CreateUserPool`: AWS rejects
38685
- * `CreateUserPool` with `MfaConfiguration: ON/OPTIONAL` unless the pool already
38686
- * has SMS configured (+ phone_number auto-verification) OR software-token MFA
38687
- * enabled — but software-token / email-OTP MFA can only be enabled via the
38688
- * post-create `SetUserPoolMfaConfig` call, not on `CreateUserPool`. So the
38689
- * correct sequence is: `CreateUserPool` WITHOUT `MfaConfiguration` (defaults
38690
- * OFF) -> `SetUserPoolMfaConfig` sets `MfaConfiguration` + the factor blocks
38691
- * together (the factor satisfies the MFA requirement, no SMS needed). This
38692
- * mirrors how CloudFormation/CDK sequence the two calls.
38693
- */
38694
- function hasMfaConfigProps(properties) {
38695
- const enabledMfas = Array.isArray(properties["EnabledMfas"]) ? properties["EnabledMfas"] : void 0;
38696
- return enabledMfas !== void 0 && enabledMfas.length > 0 || !!properties["EmailAuthenticationMessage"] || !!properties["EmailAuthenticationSubject"] || !!properties["WebAuthnRelyingPartyID"] || !!properties["WebAuthnUserVerification"];
38697
- }
38698
- function buildMfaConfigRequest(physicalId, properties) {
38699
- const enabledMfas = Array.isArray(properties["EnabledMfas"]) ? properties["EnabledMfas"] : void 0;
38916
+ function buildMfaConfigRequest(physicalId, properties, logger) {
38917
+ const { factors: enabledMfas, declaresFactor, malformed: enabledMfasMalformed } = readEnabledMfas(properties);
38700
38918
  const emailMessage = properties["EmailAuthenticationMessage"] || void 0;
38701
38919
  const emailSubject = properties["EmailAuthenticationSubject"] || void 0;
38702
38920
  const webAuthnRpId = properties["WebAuthnRelyingPartyID"] || void 0;
38703
38921
  const webAuthnUserVerification = properties["WebAuthnUserVerification"] || void 0;
38704
38922
  if (!hasMfaConfigProps(properties)) return void 0;
38705
38923
  const request = { UserPoolId: physicalId };
38706
- request.MfaConfiguration = properties["MfaConfiguration"] ?? "OPTIONAL";
38707
38924
  const factors = new Set(enabledMfas ?? []);
38925
+ const unrecognizedFactors = [...factors].filter((factor) => factor !== MFA_FACTOR_SMS && factor !== MFA_FACTOR_SOFTWARE_TOKEN && factor !== MFA_FACTOR_EMAIL_OTP);
38708
38926
  if (factors.has(MFA_FACTOR_SOFTWARE_TOKEN)) request.SoftwareTokenMfaConfiguration = { Enabled: true };
38709
38927
  if (factors.has(MFA_FACTOR_SMS)) request.SmsMfaConfiguration = { ...properties["SmsConfiguration"] ? { SmsConfiguration: properties["SmsConfiguration"] } : {} };
38710
38928
  if (factors.has(MFA_FACTOR_EMAIL_OTP) || emailMessage !== void 0 || emailSubject !== void 0) request.EmailMfaConfiguration = {
@@ -38715,6 +38933,15 @@ function buildMfaConfigRequest(physicalId, properties) {
38715
38933
  ...webAuthnRpId !== void 0 ? { RelyingPartyId: webAuthnRpId } : {},
38716
38934
  ...webAuthnUserVerification !== void 0 ? { UserVerification: webAuthnUserVerification } : {}
38717
38935
  };
38936
+ const enablesMfaFactor = declaresFactor || request.EmailMfaConfiguration !== void 0;
38937
+ request.MfaConfiguration = properties["MfaConfiguration"] ?? (enablesMfaFactor ? "OPTIONAL" : "OFF");
38938
+ const dropped = unrecognizedFactors.map((f) => JSON.stringify(f) ?? String(f));
38939
+ if (enabledMfasMalformed) dropped.push(`${JSON.stringify(properties["EnabledMfas"])} (not a list)`);
38940
+ if (dropped.length > 0) {
38941
+ const anyFactorBlock = request.SmsMfaConfiguration !== void 0 || request.SoftwareTokenMfaConfiguration !== void 0 || request.EmailMfaConfiguration !== void 0;
38942
+ const consequence = request.MfaConfiguration === "OFF" ? `MfaConfiguration is OFF, so this pool deploys with MFA DISABLED` : anyFactorBlock ? `MfaConfiguration is ${request.MfaConfiguration} and another factor block IS sent, so these entries are silently ignored rather than failing the call on their own` : `MfaConfiguration is ${request.MfaConfiguration} with no factor enabled, so AWS rejects this call rather than deploying the pool with MFA disabled`;
38943
+ logger?.warn(`UserPool ${physicalId}: EnabledMfas entries ${dropped.join(", ")} do not map to an MFA factor block (known: ${MFA_FACTOR_SMS}, ${MFA_FACTOR_SOFTWARE_TOKEN}, ${MFA_FACTOR_EMAIL_OTP}); no factor is enabled from them. ${consequence}.`);
38944
+ }
38718
38945
  return request;
38719
38946
  }
38720
38947
  /**
@@ -38852,7 +39079,7 @@ var CognitoUserPoolProvider = class {
38852
39079
  * (mirrors DynamoDBTableProvider.retryOnTransientControlPlane).
38853
39080
  */
38854
39081
  async applyMfaConfig(physicalId, properties) {
38855
- const request = buildMfaConfigRequest(physicalId, properties);
39082
+ const request = buildMfaConfigRequest(physicalId, properties, this.logger);
38856
39083
  if (!request) return;
38857
39084
  await this.retryOnTransientControlPlane(() => this.getClient().send(new SetUserPoolMfaConfigCommand(request)), `SetUserPoolMfaConfig(${physicalId})`);
38858
39085
  }
@@ -80107,7 +80334,7 @@ function createMigrateCommand() {
80107
80334
  */
80108
80335
  function buildProgram() {
80109
80336
  const program = new Command();
80110
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.28");
80337
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.30");
80111
80338
  program.hook("preAction", (_thisCommand, actionCommand) => {
80112
80339
  const { profile } = actionCommand.optsWithGlobals();
80113
80340
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;