@go-to-k/cdkd 0.283.34 → 0.283.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as getAccountInfo, $t as formatDockerLoginError, A as cyan, An as resolveBucketRegion, At as TemplateParser, B as stateKeySecretExposure, Bn as LocalInvokeBuildError$1, Bt as loadPublishableAssetManifest, C as makeCanonicalizePropertiesFn, Cn as expectedOwnerParam, Ct as s3BucketWebsiteUrl, D as renderStatefulReason, Dn as AssemblyReader, Dt as describeTypeWithThrottleRetry, E as isStatefulRecreateTargetSync, En as derivePartitionAndUrlSuffix, Et as INTRINSIC_KEYS, F as collectDeclaredOutputNames, Ft as AssetPublisher, G as findActionableSilentDrops, Gn as NestedStackChildDirectDestroyError, Gt as BOOTSTRAP_MARKER_PREFIX, H as collectInlinePolicyNamesManagedBySiblings, Hn as LocalStartServiceError, Ht as escapeRegExp, I as collectPublishedOutputNames, In as CdkdError, It as stringifyValue, J as slowCcOperationTimeoutMs, Jn as ResourceTimeoutError, Jt as parseBootstrapMarker, K as findSilentDropProperties, Kn as PartialFailureError, Kt as ensureAssetStorage, L as exportAliasCollisionScrubWarning, Lt as WorkGraph, M as green, Mn as getAwsClients, Mt as S3StateBackend, N as red, Nn as resetAwsClients, Nt as rebuildClientForBucketRegion, O as formatResourceLine, On as processStackMessages, Ot as withRetry, P as yellow, Pn as setAwsClients, Pt as shouldRetainResource, Q as cfnRefValueFromPhysicalId, Qt as buildDockerImage, R as isExportAliasCollision, Rt as buildAssetRedirectMap, S as unsupportedFinalSnapshotError, Sn as uploadCfnTemplate, St as s3BucketRegionalDomainName, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as canonicalizeRegion, Tt as DiffCalculator, U as clearOnUpdateRemoval, Ut as stripControlChars, V as IAMRoleProvider, Vn as LocalMigrateError, Vt as rewriteTemplateAssetReferences, W as ProviderRegistry, Wn as MissingCdkCliError, Wt as AssetModeResolver, X as isTerminationProtectionPropagationError, Xn as StackHasActiveImportsError, Xt as validateContainerRepoName, Y as disableInstanceApiTermination, Yn as ResourceUpdateNotSupportedError, Yt as validateAssetBucketName, Z as IntrinsicFunctionResolver, Zn as StackTerminationProtectionError, Zt as buildDenyExternalAccessPolicy, _ as buildFinalSnapshotIdentifier, _n as warnDeprecatedNoPrefixCliFlag, _t as redactSecretsForState, a as DeploymentEventsStore, an as Synthesizer, ar as isRetryableTransientError, at as coerceCfnBoolean, b as isFinalSnapshotError, bn as MIGRATE_TMP_PREFIX, bt as s3BucketDomainName, c as replayFailedOperations, cn as getLegacyStateBucketName, ct as readConfigString, d as deleteSkipReason, dn as resolveCaptureObservedState, dt as requireConfigObject, en as getDockerCmd, et as refStateLookupFromResource, f as withResourceDeadline, fn as resolveSkipPrefix, ft as requireConfigString, g as PRE_DELETE_SNAPSHOT_TYPES, gn as stateBucketExistenceConfirmed, gt as maskSecretsInText, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as resolveUseCdkBootstrapAssets, ht as TEMPLATE_SOURCED_RULES, i as DeploymentEventsReader, in as getDockerImageBySourceHash, ir as isMarkedNonRetryable, it as assertRegionMatch, j as gray, jn as AwsClients, jt as LockManager, k as bold, kt as DagBuilder, l as replayRollback, ln as resolveApp, lt as replayWarn, m as computeImplicitDeleteEdges, mn as resolveStateBucketWithDefaultAndSource, mt as STATE_SOURCED_READBACK_RULES, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as runDockerStreaming, nr as normalizeAwsError, nt as normalizeAwsTagsToCfn, o as planFailedOps, on as synthesisStatusMessage, or as isThrottlingError, ot as configBooleanRefusal, p as IMPLICIT_DELETE_DEPENDENCIES, pn as resolveStateBucketWithDefault, pt as STATE_SOURCED_CROSS_GENERATION_RULES, q as CloudControlProvider, qn as ProvisioningError, qt as getBootstrapMarkerKey, r as DeployEngine, rn as AssetManifestLoader, rr as withErrorHandling, rt as resolveExplicitPhysicalId, s as planRollback, sn as getDefaultStateBucketName, sr as markNonRetryable, st as configStringRefusal, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as runDockerForeground, tt as WAFv2WebACLProvider, u as UNSPECIFIED_SKIP_REASON, un as resolveAutoAssetStorage, ut as requireConfigArray, v as ccRoutedFinalSnapshotError, vn as CFN_TEMPLATE_BODY_LIMIT, vt as scrubResourceRecord, w as extractDeploymentEventError, wn as PARTITION_TABLE, wt as applyRoleArnIfSet, x as refusesFinalSnapshot, xn as findLargeInlineResources, xt as s3BucketDualStackDomainName, y as createPreDeleteFinalSnapshot, yn as CFN_TEMPLATE_URL_LIMIT, yt as s3BucketArn, z as secretBearingStateKeyWarning, zn as DeployCancelledError, zt as createAssetRedirectResolver } from "./deploy-engine-DlEg9thJ.js";
2
+ import { $ as getAccountInfo, $t as formatDockerLoginError, A as cyan, An as resolveBucketRegion, At as TemplateParser, B as stateKeySecretExposure, Bn as LocalInvokeBuildError$1, Bt as loadPublishableAssetManifest, C as makeCanonicalizePropertiesFn, Cn as expectedOwnerParam, Ct as s3BucketWebsiteUrl, D as renderStatefulReason, Dn as AssemblyReader, Dt as describeTypeWithThrottleRetry, E as isStatefulRecreateTargetSync, En as derivePartitionAndUrlSuffix, Et as INTRINSIC_KEYS, F as collectDeclaredOutputNames, Ft as AssetPublisher, G as findActionableSilentDrops, Gn as NestedStackChildDirectDestroyError, Gt as BOOTSTRAP_MARKER_PREFIX, H as collectInlinePolicyNamesManagedBySiblings, Hn as LocalStartServiceError, Ht as escapeRegExp, I as collectPublishedOutputNames, In as CdkdError, It as stringifyValue, J as slowCcOperationTimeoutMs, Jn as ResourceTimeoutError, Jt as parseBootstrapMarker, K as findSilentDropProperties, Kn as PartialFailureError, Kt as ensureAssetStorage, L as exportAliasCollisionScrubWarning, Lt as WorkGraph, M as green, Mn as getAwsClients, Mt as S3StateBackend, N as red, Nn as resetAwsClients, Nt as rebuildClientForBucketRegion, O as formatResourceLine, On as processStackMessages, Ot as withRetry, P as yellow, Pn as setAwsClients, Pt as shouldRetainResource, Q as cfnRefValueFromPhysicalId, Qt as buildDockerImage, R as isExportAliasCollision, Rt as buildAssetRedirectMap, S as unsupportedFinalSnapshotError, Sn as uploadCfnTemplate, St as s3BucketRegionalDomainName, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as canonicalizeRegion, Tt as DiffCalculator, U as clearOnUpdateRemoval, Ut as stripControlChars, V as IAMRoleProvider, Vn as LocalMigrateError, Vt as rewriteTemplateAssetReferences, W as ProviderRegistry, Wn as MissingCdkCliError, Wt as AssetModeResolver, X as isTerminationProtectionPropagationError, Xn as StackHasActiveImportsError, Xt as validateContainerRepoName, Y as disableInstanceApiTermination, Yn as ResourceUpdateNotSupportedError, Yt as validateAssetBucketName, Z as IntrinsicFunctionResolver, Zn as StackTerminationProtectionError, Zt as buildDenyExternalAccessPolicy, _ as buildFinalSnapshotIdentifier, _n as warnDeprecatedNoPrefixCliFlag, _t as redactSecretsForState, a as DeploymentEventsStore, an as Synthesizer, ar as isRetryableTransientError, at as coerceCfnBoolean, b as isFinalSnapshotError, bn as MIGRATE_TMP_PREFIX, bt as s3BucketDomainName, c as replayFailedOperations, cn as getLegacyStateBucketName, ct as readConfigString, d as deleteSkipReason, dn as resolveCaptureObservedState, dt as requireConfigObject, en as getDockerCmd, et as refStateLookupFromResource, f as withResourceDeadline, fn as resolveSkipPrefix, ft as requireConfigString, g as PRE_DELETE_SNAPSHOT_TYPES, gn as stateBucketExistenceConfirmed, gt as maskSecretsInText, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as resolveUseCdkBootstrapAssets, ht as TEMPLATE_SOURCED_RULES, i as DeploymentEventsReader, in as getDockerImageBySourceHash, ir as isMarkedNonRetryable, it as assertRegionMatch, j as gray, jn as AwsClients, jt as LockManager, k as bold, kt as DagBuilder, l as replayRollback, ln as resolveApp, lt as replayWarn, m as computeImplicitDeleteEdges, mn as resolveStateBucketWithDefaultAndSource, mt as STATE_SOURCED_READBACK_RULES, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as runDockerStreaming, nr as normalizeAwsError, nt as normalizeAwsTagsToCfn, o as planFailedOps, on as synthesisStatusMessage, or as isThrottlingError, ot as configBooleanRefusal, p as IMPLICIT_DELETE_DEPENDENCIES, pn as resolveStateBucketWithDefault, pt as STATE_SOURCED_CROSS_GENERATION_RULES, q as CloudControlProvider, qn as ProvisioningError, qt as getBootstrapMarkerKey, r as DeployEngine, rn as AssetManifestLoader, rr as withErrorHandling, rt as resolveExplicitPhysicalId, s as planRollback, sn as getDefaultStateBucketName, sr as markNonRetryable, st as configStringRefusal, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as runDockerForeground, tt as WAFv2WebACLProvider, u as UNSPECIFIED_SKIP_REASON, un as resolveAutoAssetStorage, ut as requireConfigArray, v as ccRoutedFinalSnapshotError, vn as CFN_TEMPLATE_BODY_LIMIT, vt as scrubResourceRecord, w as extractDeploymentEventError, wn as PARTITION_TABLE, wt as applyRoleArnIfSet, x as refusesFinalSnapshot, xn as findLargeInlineResources, xt as s3BucketDualStackDomainName, y as createPreDeleteFinalSnapshot, yn as CFN_TEMPLATE_URL_LIMIT, yt as s3BucketArn, z as secretBearingStateKeyWarning, zn as DeployCancelledError, zt as createAssetRedirectResolver } from "./deploy-engine-Bhq_JPCc.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-B1AQpZfU.js";
4
+ import { t as ASGProvider } from "./asg-provider-BruE_3F2.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";
@@ -38931,6 +38931,46 @@ const MFA_FACTOR_SMS = "SMS_MFA";
38931
38931
  const MFA_FACTOR_SOFTWARE_TOKEN = "SOFTWARE_TOKEN_MFA";
38932
38932
  const MFA_FACTOR_EMAIL_OTP = "EMAIL_OTP";
38933
38933
  /**
38934
+ * CFn path every `MfaConfiguration` shape refusal in this provider reports.
38935
+ * One constant so the three sites that READ the property -- `create`,
38936
+ * `update`, and `narrowMfaConfiguration` (the diff / effectiveProperties
38937
+ * side) -- cannot word the same fault differently. `buildMfaConfigRequest`
38938
+ * performs no read of its own; it is handed the already-guarded value.
38939
+ */
38940
+ const MFA_CONFIGURATION_PATH = "AWS::Cognito::UserPool MfaConfiguration";
38941
+ function classifyDeclaredMfaConfiguration(raw) {
38942
+ if (raw === void 0) return "absent";
38943
+ if (typeof raw !== "string") return "refused";
38944
+ return raw.trim() === "" ? "blank" : "usable";
38945
+ }
38946
+ /**
38947
+ * Read the template's `MfaConfiguration` through the shape guard AND fold a
38948
+ * TRIMMED-BLANK value to absence.
38949
+ *
38950
+ * The fold is the load-bearing half. `requireConfigString` short-circuits on
38951
+ * `fallback === '' && typeof value === 'string'` (config-shape.ts), so against
38952
+ * the blank fallback this site uses it returns ANY string verbatim --
38953
+ * including `' '`. That value is truthy, so it rode the
38954
+ * `if (mfaConfiguration && ...)` gates onto `CreateUserPool` / `UpdateUserPool`
38955
+ * and the `|| default` in `buildMfaConfigRequest` onto `SetUserPoolMfaConfig`,
38956
+ * where AWS rejects the enum and the deploy fails -- while the blank-value
38957
+ * warning promised "the pool deploys with the default MFA configuration
38958
+ * instead". Two spellings of blank behaved differently and the message
38959
+ * described only one of them.
38960
+ *
38961
+ * Folding here rather than widening the guard keeps the change local to the
38962
+ * one site whose fallback is blank, and makes the wire behavior match what the
38963
+ * warning says: a blank declares nothing, so the default applies.
38964
+ *
38965
+ * Every read site MUST go through this, including `narrowMfaConfiguration` --
38966
+ * a diff side that folded differently from the wire is the phantom drift this
38967
+ * whole mechanism exists to remove.
38968
+ */
38969
+ function readDeclaredMfaConfiguration(raw, options) {
38970
+ const value = requireConfigString(raw, "", MFA_CONFIGURATION_PATH, options);
38971
+ return value.trim() === "" ? "" : value;
38972
+ }
38973
+ /**
38934
38974
  * Read `EnabledMfas` as the pair (recognized list, "the template asked for a
38935
38975
  * factor at all").
38936
38976
  *
@@ -38954,14 +38994,16 @@ function readEnabledMfas(properties) {
38954
38994
  return {
38955
38995
  factors,
38956
38996
  declaresFactor: factors.length > 0,
38957
- malformed: false
38997
+ malformed: false,
38998
+ blank: false
38958
38999
  };
38959
39000
  }
38960
39001
  const malformed = raw !== void 0 && raw !== null && raw !== "";
38961
39002
  return {
38962
39003
  factors: void 0,
38963
39004
  declaresFactor: malformed,
38964
- malformed
39005
+ malformed,
39006
+ blank: raw === ""
38965
39007
  };
38966
39008
  }
38967
39009
  /**
@@ -39000,9 +39042,17 @@ function hasMfaConfigProps(properties) {
39000
39042
  * enables an MFA FACTOR — `OPTIONAL` if it does, `OFF` (CloudFormation's own
39001
39043
  * default) if it does not. The body comment on that decision explains why both
39002
39044
  * halves of the factor test are load-bearing.
39045
+ *
39046
+ * `declaredMfaConfiguration` is the caller's ALREADY-GUARDED read of
39047
+ * `properties['MfaConfiguration']` (issue #1925 item 2), with `''` meaning
39048
+ * "the template declared none, or declared a value the shape guard refused".
39049
+ * The read happens at the caller so it runs ONCE per deploy path — the value
39050
+ * is also needed for the `CreateUserPool` / `UpdateUserPool` forward — and so
39051
+ * the create path can REFUSE a malformed value while the update path only
39052
+ * warns, which is the split `requireConfigString`'s `onUnusable` encodes.
39003
39053
  */
39004
- function buildMfaConfigRequest(physicalId, properties, logger) {
39005
- const { factors: enabledMfas, declaresFactor, malformed: enabledMfasMalformed } = readEnabledMfas(properties);
39054
+ function buildMfaConfigRequest(physicalId, properties, logger, declaredMfaConfiguration = "") {
39055
+ const { factors: enabledMfas, declaresFactor, malformed: enabledMfasMalformed, blank: enabledMfasBlank } = readEnabledMfas(properties);
39006
39056
  const emailMessage = properties["EmailAuthenticationMessage"] || void 0;
39007
39057
  const emailSubject = properties["EmailAuthenticationSubject"] || void 0;
39008
39058
  const webAuthnRpId = properties["WebAuthnRelyingPartyID"] || void 0;
@@ -39022,17 +39072,83 @@ function buildMfaConfigRequest(physicalId, properties, logger) {
39022
39072
  ...webAuthnUserVerification !== void 0 ? { UserVerification: webAuthnUserVerification } : {}
39023
39073
  };
39024
39074
  const enablesMfaFactor = declaresFactor || request.EmailMfaConfiguration !== void 0;
39025
- request.MfaConfiguration = properties["MfaConfiguration"] ?? (enablesMfaFactor ? "OPTIONAL" : "OFF");
39075
+ request.MfaConfiguration = declaredMfaConfiguration || (enablesMfaFactor ? "OPTIONAL" : "OFF");
39076
+ if (enabledMfasBlank) logger?.warn(`UserPool ${physicalId}: EnabledMfas is an empty string, which declares no MFA factor and is treated as absent — a mangled Fn::Join or an empty String parameter collapses to this shape. Nothing is enabled from it and MfaConfiguration is ${request.MfaConfiguration}; declare the factor names as a list (e.g. ["${MFA_FACTOR_SOFTWARE_TOKEN}"]) if a factor was intended.`);
39077
+ const anyFactorBlock = request.SmsMfaConfiguration !== void 0 || request.SoftwareTokenMfaConfiguration !== void 0 || request.EmailMfaConfiguration !== void 0;
39078
+ if (request.MfaConfiguration === "OFF" && anyFactorBlock) logger?.warn(`UserPool ${physicalId}: MfaConfiguration is pinned to OFF while an MFA factor block is configured. The factor configuration is still sent, but the pool deploys with MFA DISABLED. Remove MfaConfiguration (or set it to ON / OPTIONAL) to enable the declared factor.`);
39026
39079
  const dropped = unrecognizedFactors.map((f) => JSON.stringify(f) ?? String(f));
39027
39080
  if (enabledMfasMalformed) dropped.push(`${JSON.stringify(properties["EnabledMfas"])} (not a list)`);
39028
39081
  if (dropped.length > 0) {
39029
- const anyFactorBlock = request.SmsMfaConfiguration !== void 0 || request.SoftwareTokenMfaConfiguration !== void 0 || request.EmailMfaConfiguration !== void 0;
39030
39082
  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`;
39031
39083
  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}.`);
39032
39084
  }
39033
39085
  return request;
39034
39086
  }
39035
39087
  /**
39088
+ * The `MfaConfiguration` cdkd actually SENDS for this bag, or `undefined` when
39089
+ * no call carries the field at all (nothing declared and no MFA-routed
39090
+ * property, so AWS applies its own default of OFF).
39091
+ *
39092
+ * Exactly one call ever carries it: `SetUserPoolMfaConfig` owns it whenever any
39093
+ * MFA-routed property is present -- and always sets it, because that API is a
39094
+ * full replace -- otherwise the `CreateUserPool` / `UpdateUserPool` forward
39095
+ * carries it, and only when it is non-blank.
39096
+ *
39097
+ * Delegates to {@link buildMfaConfigRequest} rather than re-deriving the
39098
+ * OPTIONAL/OFF default, so the recorded value and the wire value cannot
39099
+ * disagree. That function is pure and synchronous, which is what lets
39100
+ * `canonicalizeDesiredProperties` -- which runs inside the diff -- call it too.
39101
+ * The empty `physicalId` and absent logger are unused on this path: every
39102
+ * message it can emit is behind `logger?.`, so passing none makes the call
39103
+ * silent, which is required of the diff side.
39104
+ */
39105
+ function resolveSentMfaConfiguration(properties, declaredMfaConfiguration) {
39106
+ const request = buildMfaConfigRequest("", properties, void 0, declaredMfaConfiguration);
39107
+ if (request) return request.MfaConfiguration;
39108
+ return declaredMfaConfiguration || void 0;
39109
+ }
39110
+ /**
39111
+ * Replace a DECLARED-but-unusable `MfaConfiguration` with the value cdkd
39112
+ * actually sends, or drop the key when nothing is sent. Returns the input
39113
+ * object unchanged when nothing applies, so the common case compares
39114
+ * byte-for-byte as before.
39115
+ *
39116
+ * ONE helper feeding BOTH sides of the decision, per `.claude/rules/
39117
+ * providers.md`: the provisioning path returns it as `effectiveProperties` so
39118
+ * STATE describes what AWS holds, and `canonicalizeDesiredProperties` applies
39119
+ * it to the DESIRED bag so the next diff compares the same thing. Shipping
39120
+ * only the first half is worse than shipping neither -- the template would keep
39121
+ * declaring the malformed value, and every later deploy would read the
39122
+ * narrowing back as a user-made change.
39123
+ *
39124
+ * Without this, all three substitution arms (the replay-CREATE downgrade, the
39125
+ * update-path warn-and-default, and the blank-string default) recorded the
39126
+ * declared `null` / `''` while AWS held `OPTIONAL` / `OFF` -- permanent phantom
39127
+ * drift that `cdkd drift --revert` re-issues forever with nothing to apply.
39128
+ *
39129
+ * A value that is merely ABSENT is deliberately NOT filled in. Reaching for
39130
+ * `effectiveProperties` is licensed only where the provider KNOWS it
39131
+ * substituted something it was told, and an omitted property was never a
39132
+ * declaration to contradict; writing the resolved default into the DESIRED
39133
+ * baseline there would also disable the #1160 absent-field removal derivation,
39134
+ * which reads that side.
39135
+ */
39136
+ function narrowMfaConfiguration(properties) {
39137
+ const raw = properties["MfaConfiguration"];
39138
+ if (raw === void 0) return properties;
39139
+ const sent = resolveSentMfaConfiguration(properties, readDeclaredMfaConfiguration(raw, { onUnusable: () => {} }));
39140
+ if (sent === raw) return properties;
39141
+ if (sent === void 0) {
39142
+ const withoutMfaConfiguration = { ...properties };
39143
+ delete withoutMfaConfiguration["MfaConfiguration"];
39144
+ return withoutMfaConfiguration;
39145
+ }
39146
+ return {
39147
+ ...properties,
39148
+ MfaConfiguration: sent
39149
+ };
39150
+ }
39151
+ /**
39036
39152
  * AWS Cognito User Pool Provider
39037
39153
  *
39038
39154
  * Implements resource provisioning for AWS::Cognito::UserPool using the Cognito SDK.
@@ -39076,6 +39192,50 @@ var CognitoUserPoolProvider = class {
39076
39192
  "WebAuthnUserVerification"
39077
39193
  ])]]);
39078
39194
  unhandledByDesign = /* @__PURE__ */ new Map([["AWS::Cognito::UserPool", /* @__PURE__ */ new Map([["WebAuthnFactorConfiguration", "No SDK wire path: @aws-sdk/client-cognito-identity-provider has no field accepting SINGLE_FACTOR | MULTI_FACTOR_WITH_USER_VERIFICATION (not on CreateUserPool/UpdateUserPool, nor SetUserPoolMfaConfig.WebAuthnConfiguration which only carries RelyingPartyId/UserVerification); CC-API-registry-only property"]])]]);
39195
+ /**
39196
+ * Warn when `MfaConfiguration` is declared as a BLANK string (issue #1925
39197
+ * item 2, review round 2).
39198
+ *
39199
+ * `requireConfigString` accepts any string against a blank fallback, so `''`
39200
+ * passes the shape guard and is then substituted by the OPTIONAL/OFF default
39201
+ * downstream. That is the exact silent-default class the guard exists to
39202
+ * kill, reached one shape further in: on main a WebAuthn-only pool declaring
39203
+ * `MfaConfiguration: ''` failed LOUDLY with an AWS enum rejection, and with
39204
+ * the guard alone it would deploy MFA OFF with no message anywhere. A
39205
+ * collapsed `Fn::Join` or an empty `String` parameter produces exactly this.
39206
+ *
39207
+ * The twin of the blank-`EnabledMfas` warning one property over, and warned
39208
+ * for the same reason it is: the value stays absence ON THE WIRE (the
39209
+ * substitution is what CloudFormation's own default does), so only the
39210
+ * silence is removed.
39211
+ *
39212
+ * `.trim()` matches the fold in {@link readDeclaredMfaConfiguration}, and the
39213
+ * two must agree: `requireConfigString` does NOT treat whitespace-only as
39214
+ * blank against the blank fallback this site uses, so before that fold a
39215
+ * `' '` was warned about here and then sent to AWS verbatim -- this message
39216
+ * describing a default that never applied.
39217
+ */
39218
+ warnOnBlankMfaConfiguration(raw) {
39219
+ if (typeof raw !== "string" || raw.trim() !== "") return;
39220
+ this.logger.warn("AWS::Cognito::UserPool MfaConfiguration is an empty string, which declares no MFA mode and is treated as absent -- a collapsed Fn::Join or an empty String parameter produces this shape. The pool deploys with the default MFA configuration instead; declare ON / OPTIONAL / OFF explicitly, or omit the property, to say which was meant.");
39221
+ }
39222
+ /**
39223
+ * The DIFF-side half of the `MfaConfiguration` substitution record.
39224
+ *
39225
+ * Applied by `DiffCalculator` to BOTH comparison sides, so a template that
39226
+ * declares a malformed / blank `MfaConfiguration` compares equal to the state
39227
+ * record holding the value cdkd actually sent for it. Without this half, the
39228
+ * `effectiveProperties` above would make every later deploy read the
39229
+ * substitution back as a user-made change and re-run the update forever.
39230
+ *
39231
+ * Shares ONE helper with the provisioning path rather than re-deriving the
39232
+ * rule, per `.claude/rules/providers.md` -- state and template narrowed by
39233
+ * different code is how this fix would become the bug.
39234
+ */
39235
+ canonicalizeDesiredProperties(resourceType, properties) {
39236
+ if (resourceType !== "AWS::Cognito::UserPool") return properties;
39237
+ return narrowMfaConfiguration(properties);
39238
+ }
39079
39239
  getClient() {
39080
39240
  if (!this.cognitoClient) this.cognitoClient = new CognitoIdentityProviderClient(this.providerRegion ? { region: this.providerRegion } : {});
39081
39241
  return this.cognitoClient;
@@ -39096,9 +39256,16 @@ var CognitoUserPoolProvider = class {
39096
39256
  /**
39097
39257
  * Create a Cognito User Pool
39098
39258
  */
39099
- async create(logicalId, resourceType, properties) {
39259
+ async create(logicalId, resourceType, properties, context) {
39100
39260
  this.logger.debug(`Creating Cognito User Pool ${logicalId}`);
39101
39261
  const poolName = properties["UserPoolName"] || generateResourceName(logicalId, { maxLength: 128 });
39262
+ let mfaConfiguration;
39263
+ try {
39264
+ mfaConfiguration = readDeclaredMfaConfiguration(properties["MfaConfiguration"], replayWarn(this.logger, context));
39265
+ this.warnOnBlankMfaConfiguration(properties["MfaConfiguration"]);
39266
+ } catch (error) {
39267
+ throw new ProvisioningError(error instanceof Error ? error.message : String(error), resourceType, logicalId, poolName, error instanceof Error ? error : void 0);
39268
+ }
39102
39269
  let createdUserPoolId;
39103
39270
  try {
39104
39271
  const createParams = { PoolName: poolName };
@@ -39110,7 +39277,7 @@ var CognitoUserPoolProvider = class {
39110
39277
  }
39111
39278
  if (properties["Schema"]) createParams.Schema = properties["Schema"];
39112
39279
  if (properties["LambdaConfig"]) createParams.LambdaConfig = properties["LambdaConfig"];
39113
- if (properties["MfaConfiguration"] && !hasMfaConfigProps(properties)) createParams.MfaConfiguration = properties["MfaConfiguration"];
39280
+ if (mfaConfiguration && !hasMfaConfigProps(properties)) createParams.MfaConfiguration = mfaConfiguration;
39114
39281
  if (properties["UserPoolTags"]) createParams.UserPoolTags = properties["UserPoolTags"];
39115
39282
  if (properties["AdminCreateUserConfig"]) createParams.AdminCreateUserConfig = properties["AdminCreateUserConfig"];
39116
39283
  if (properties["AccountRecoverySetting"]) createParams.AccountRecoverySetting = properties["AccountRecoverySetting"];
@@ -39137,8 +39304,9 @@ var CognitoUserPoolProvider = class {
39137
39304
  const { urlSuffix } = derivePartitionAndUrlSuffix(region);
39138
39305
  const providerName = `cognito-idp.${region}.${urlSuffix}/${userPoolId}`;
39139
39306
  const providerUrl = `https://cognito-idp.${region}.${urlSuffix}/${userPoolId}`;
39140
- await this.applyMfaConfig(userPoolId, properties);
39307
+ await this.applyMfaConfig(userPoolId, properties, mfaConfiguration);
39141
39308
  this.logger.debug(`Successfully created Cognito User Pool ${logicalId}: ${userPoolId}`);
39309
+ const effectiveProperties = narrowMfaConfiguration(properties);
39142
39310
  return {
39143
39311
  physicalId: userPoolId,
39144
39312
  attributes: {
@@ -39146,7 +39314,8 @@ var CognitoUserPoolProvider = class {
39146
39314
  ProviderName: providerName,
39147
39315
  ProviderURL: providerUrl,
39148
39316
  UserPoolId: userPoolId
39149
- }
39317
+ },
39318
+ ...effectiveProperties !== properties ? { effectiveProperties } : {}
39150
39319
  };
39151
39320
  } catch (error) {
39152
39321
  if (createdUserPoolId) try {
@@ -39166,10 +39335,109 @@ var CognitoUserPoolProvider = class {
39166
39335
  * control-plane writes on a freshly-created pool can briefly conflict
39167
39336
  * (mirrors DynamoDBTableProvider.retryOnTransientControlPlane).
39168
39337
  */
39169
- async applyMfaConfig(physicalId, properties) {
39170
- const request = buildMfaConfigRequest(physicalId, properties, this.logger);
39338
+ async applyMfaConfig(physicalId, properties, declaredMfaConfiguration, options = {}) {
39339
+ const request = buildMfaConfigRequest(physicalId, properties, this.logger, declaredMfaConfiguration);
39171
39340
  if (!request) return;
39172
39341
  await this.retryOnTransientControlPlane(() => this.getClient().send(new SetUserPoolMfaConfigCommand(request)), `SetUserPoolMfaConfig(${physicalId})`);
39342
+ this.reportMfaConfigurationOff(physicalId, request, declaredMfaConfiguration, options);
39343
+ }
39344
+ /**
39345
+ * Say so when this deploy sends `MfaConfiguration=OFF` without the template
39346
+ * having asked for OFF (issue #1925, third item).
39347
+ *
39348
+ * Everything here keys on the BUILT request, so there is exactly one
39349
+ * definition of "this deploy resolves to OFF", and on `declaredMfaConfiguration
39350
+ * === ''`, i.e. the template did not pin a usable value. `create()` passes
39351
+ * neither a live value nor a `declaredKind`, so it reports nothing -- correct,
39352
+ * since a fresh pool has no value to downgrade.
39353
+ *
39354
+ * Three outcomes rather than one, because the live value can be KNOWN,
39355
+ * known-and-harmless, or UNAVAILABLE, and only the first is a downgrade that
39356
+ * can be named:
39357
+ *
39358
+ * - live `ON` / `OPTIONAL` -- a real downgrade; name the value being lost.
39359
+ * - live `OFF` -- not a downgrade at all; say nothing. This is the arm the
39360
+ * live-value test protects, and it is load-bearing in the SILENT
39361
+ * direction: without it every already-OFF pool and every failed probe
39362
+ * warned, the latter reading `live value was undefined`.
39363
+ * - live UNKNOWN (the probe failed) -- the case that made this necessary.
39364
+ * A malformed but TRUTHY value used to reach `UpdateUserPool`, AWS
39365
+ * rejected the enum, MFA stayed as it was and the deploy failed; now the
39366
+ * guard substitutes and the deploy exits 0 with MFA off. On a role lacking
39367
+ * `cognito-idp:GetUserPoolMfaConfig` -- exactly the role this arm exists
39368
+ * for -- the probe returns nothing, so without this the ONLY path that got
39369
+ * quieter than main would say nothing at all.
39370
+ */
39371
+ reportMfaConfigurationOff(physicalId, request, declaredMfaConfiguration, options) {
39372
+ if (declaredMfaConfiguration !== "" || request.MfaConfiguration !== "OFF") return;
39373
+ const kind = options.declaredKind ?? "absent";
39374
+ const cause = kind === "refused" ? "the declared MfaConfiguration was refused as malformed (see the warning above), so this update sent MfaConfiguration=OFF instead" : kind === "blank" ? "the declared MfaConfiguration is blank, so it is treated as absent and this update sent MfaConfiguration=OFF" : "the template declares no MfaConfiguration, so this update sent MfaConfiguration=OFF (CloudFormation's own default)";
39375
+ const remedy = (value) => kind === "absent" ? `Declare MfaConfiguration: ${value} in the template to keep it.` : `Repair MfaConfiguration to a literal ${value} to keep it.`;
39376
+ if (options.liveMfaConfiguration === "ON" || options.liveMfaConfiguration === "OPTIONAL") {
39377
+ this.logger.warn(`UserPool ${physicalId}: ${cause} to a pool whose live value was ${options.liveMfaConfiguration} — SetUserPoolMfaConfig is a full replace, so MFA is now OFF. ${remedy(options.liveMfaConfiguration)}`);
39378
+ return;
39379
+ }
39380
+ if (options.liveReadFailed !== true) return;
39381
+ const declaredClause = kind === "absent" ? `the template declares no MfaConfiguration` : `the declared MfaConfiguration could not be used`;
39382
+ this.logger.warn(`UserPool ${physicalId}: ${declaredClause}, so this update sent MfaConfiguration=OFF — and the pool's previous MFA setting could not be read, so cdkd cannot say whether that turned MFA off. Grant cognito-idp:GetUserPoolMfaConfig to the deploy role to have this reported precisely. ${remedy("ON or OPTIONAL")}`);
39383
+ }
39384
+ /**
39385
+ * Read the pool's live `MfaConfiguration` for the undeclared-downgrade
39386
+ * announcement (issue #1925, third item), or `undefined` when it cannot be
39387
+ * determined.
39388
+ *
39389
+ * `SetUserPoolMfaConfig` is a full replace and this provider re-issues it on
39390
+ * every update carrying an MFA-routed property, unconditioned on the MFA
39391
+ * configuration having CHANGED. So a WebAuthn-only template applied to a pool
39392
+ * whose live `MfaConfiguration` is `ON` / `OPTIONAL` — console drift, or a
39393
+ * `cdkd import` of an MFA-enabled pool — writes `OFF` on the next unrelated
39394
+ * property change. That is defensible template-is-truth / CloudFormation
39395
+ * parity and is NOT changed here; only its silence is.
39396
+ *
39397
+ * **Called BEFORE `UpdateUserPool`, defensively — NOT because AWS resets the
39398
+ * field.** MEASURED us-east-1 2026-08-18: a pool at `ON` with software-token
39399
+ * enabled keeps `ON` through an `UpdateUserPool` that omits
39400
+ * `MfaConfiguration`, confirmed by both `get-user-pool-mfa-config` and
39401
+ * `describe-user-pool`, with controls proving the call really ran (a
39402
+ * `--auto-verified-attributes` change in the same request applied) and that
39403
+ * the field is writable here rather than ignored (an explicit
39404
+ * `--mfa-configuration OFF` did reset it). So AWS's blanket "unspecified
39405
+ * parameters are set to their default value" holds field by field —
39406
+ * `AutoVerifiedAttributes` DOES reset, `MfaConfiguration` does not — and
39407
+ * there is no MFA-OFF window to design around.
39408
+ *
39409
+ * The earlier ordering was reached from that doc sentence alone and is kept
39410
+ * only because it is strictly safer and already tested: reading first cannot
39411
+ * report a value this same call clobbered, whatever AWS does later. Do not
39412
+ * re-derive a redesign from the doc sentence — it has been measured.
39413
+ *
39414
+ * Best-effort by construction: the read exists only to ANNOUNCE, so a
39415
+ * failure must not fail the update the user asked for.
39416
+ *
39417
+ * **This adds `cognito-idp:GetUserPoolMfaConfig` to the permissions a deploy
39418
+ * role wants.** The action was previously needed only by `readCurrentState`
39419
+ * (drift / import), so a role scoped to deploying could legitimately lack it.
39420
+ *
39421
+ * A failure is REPORTED rather than swallowed, but not here: this method's
39422
+ * gate is a deliberate SUPERSET of the announcement condition (see the call
39423
+ * site in `update()`), so warning at the point of failure fired on templates
39424
+ * that structurally cannot downgrade -- an `EnabledMfas` with no
39425
+ * `MfaConfiguration` resolves to OPTIONAL, and a least-privileged role saw
39426
+ * "cdkd cannot say whether it turns MFA off" on every deploy of it. The flag
39427
+ * returned here is consumed by `reportMfaConfigurationOff`, which speaks only
39428
+ * where the request actually resolves to OFF.
39429
+ */
39430
+ async readLiveMfaConfiguration(physicalId) {
39431
+ try {
39432
+ const live = await this.getClient().send(new GetUserPoolMfaConfigCommand({ UserPoolId: physicalId }));
39433
+ return live.MfaConfiguration !== void 0 ? {
39434
+ value: live.MfaConfiguration,
39435
+ failed: false
39436
+ } : { failed: false };
39437
+ } catch (error) {
39438
+ this.logger.debug(`GetUserPoolMfaConfig failed for UserPool ${physicalId} (${error instanceof Error ? error.name : typeof error}): ${error instanceof Error ? error.message : String(error)}`);
39439
+ return { failed: true };
39440
+ }
39173
39441
  }
39174
39442
  /**
39175
39443
  * Retry a Cognito control-plane call on transient "settling" errors. A
@@ -39199,6 +39467,9 @@ var CognitoUserPoolProvider = class {
39199
39467
  */
39200
39468
  async update(logicalId, physicalId, resourceType, properties, previousProperties) {
39201
39469
  this.logger.debug(`Updating Cognito User Pool ${logicalId}: ${physicalId}`);
39470
+ const mfaConfiguration = readDeclaredMfaConfiguration(properties["MfaConfiguration"], { onUnusable: (message) => this.logger.warn(message) });
39471
+ this.warnOnBlankMfaConfiguration(properties["MfaConfiguration"]);
39472
+ const liveMfa = mfaConfiguration === "" && hasMfaConfigProps(properties) ? await this.readLiveMfaConfiguration(physicalId) : void 0;
39202
39473
  try {
39203
39474
  const updateParams = { UserPoolId: physicalId };
39204
39475
  if (properties["Policies"]) {
@@ -39207,7 +39478,7 @@ var CognitoUserPoolProvider = class {
39207
39478
  }
39208
39479
  if (properties["LambdaConfig"]) updateParams.LambdaConfig = properties["LambdaConfig"];
39209
39480
  if (properties["AutoVerifiedAttributes"]) updateParams.AutoVerifiedAttributes = properties["AutoVerifiedAttributes"];
39210
- if (properties["MfaConfiguration"]) updateParams.MfaConfiguration = properties["MfaConfiguration"];
39481
+ if (mfaConfiguration && !hasMfaConfigProps(properties)) updateParams.MfaConfiguration = mfaConfiguration;
39211
39482
  if (properties["AdminCreateUserConfig"]) updateParams.AdminCreateUserConfig = properties["AdminCreateUserConfig"];
39212
39483
  if (properties["AccountRecoverySetting"]) updateParams.AccountRecoverySetting = properties["AccountRecoverySetting"];
39213
39484
  if (properties["UserPoolTags"]) updateParams.UserPoolTags = properties["UserPoolTags"];
@@ -39225,13 +39496,18 @@ var CognitoUserPoolProvider = class {
39225
39496
  if (properties["UserPoolTier"]) updateParams.UserPoolTier = properties["UserPoolTier"];
39226
39497
  await this.getClient().send(new UpdateUserPoolCommand(updateParams));
39227
39498
  await this.reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, properties["Schema"], previousProperties["Schema"]);
39228
- await this.applyMfaConfig(physicalId, properties);
39499
+ await this.applyMfaConfig(physicalId, properties, mfaConfiguration, {
39500
+ ...liveMfa?.value !== void 0 ? { liveMfaConfiguration: liveMfa.value } : {},
39501
+ ...liveMfa?.failed === true ? { liveReadFailed: true } : {},
39502
+ declaredKind: classifyDeclaredMfaConfiguration(properties["MfaConfiguration"])
39503
+ });
39229
39504
  this.logger.debug(`Successfully updated Cognito User Pool ${logicalId}`);
39230
39505
  const userPool = (await this.getClient().send(new DescribeUserPoolCommand({ UserPoolId: physicalId }))).UserPool;
39231
39506
  const region = await this.getClient().config.region();
39232
39507
  const { urlSuffix } = derivePartitionAndUrlSuffix(region);
39233
39508
  const providerName = `cognito-idp.${region}.${urlSuffix}/${physicalId}`;
39234
39509
  const providerUrl = `https://cognito-idp.${region}.${urlSuffix}/${physicalId}`;
39510
+ const effectiveProperties = narrowMfaConfiguration(properties);
39235
39511
  return {
39236
39512
  physicalId,
39237
39513
  wasReplaced: false,
@@ -39240,7 +39516,8 @@ var CognitoUserPoolProvider = class {
39240
39516
  ProviderName: providerName,
39241
39517
  ProviderURL: providerUrl,
39242
39518
  UserPoolId: physicalId
39243
- }
39519
+ },
39520
+ ...effectiveProperties !== properties ? { effectiveProperties } : {}
39244
39521
  };
39245
39522
  } catch (error) {
39246
39523
  if (error instanceof ResourceUpdateNotSupportedError || error instanceof ProvisioningError) throw error;
@@ -80969,7 +81246,7 @@ function createMigrateCommand() {
80969
81246
  */
80970
81247
  function buildProgram() {
80971
81248
  const program = new Command();
80972
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.34");
81249
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.36");
80973
81250
  program.hook("preAction", (_thisCommand, actionCommand) => {
80974
81251
  const { profile } = actionCommand.optsWithGlobals();
80975
81252
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;