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