@go-to-k/cdkd 0.290.14 → 0.290.16

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.
@@ -1,8 +1,8 @@
1
- import { $ as secretBearing, $n as BOOTSTRAP_MARKER_PREFIX, $r as getAwsClients, $t as s3BucketRegionalDomainName, A as extractDeploymentEventError, Ai as markNonRetryable, An as buildForceUnlockCommand, Ar as resolveAutoAssetStorage, At as parameterTypeMayLoseSecretIdentity, B as toCfnInteger, Bn as importableOutputs, Br as MIGRATE_TMP_PREFIX, Bt as configIntegerRefusal, C as buildFinalSnapshotIdentifier, Cn as recordMaskOnlyValue, Cr as AssetManifestLoader, Ct as IntrinsicFunctionResolver, D as refusesFinalSnapshot, Di as isRetryableTransientError, Dn as LockManager, Dr as getDefaultStateBucketName, Dt as getAccountInfo, E as isFinalSnapshotError, Ei as isMarkedNonRetryable, En as scrubResourceRecord, Er as synthesisStatusMessage, Et as coerceParameterTypedValue, F as isStatefulRecreateTargetSync, Fn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Fr as resolveUseCdkBootstrapAssets, Ft as resolveExplicitPhysicalId, G as gray, Gn as AssetPublisher, Gr as PARTITION_TABLE, Gt as requireConfigObject, H as formatResourceLine, Hn as orphansAfterRollback, Hr as uploadCfnTemplate, Ht as readConfigString, I as renderStatefulReason, In as DEFAULT_STATE_PREFIX, Ir as stateBucketExistenceConfirmed, It as assertRegionMatch, J as yellow, Jn as buildAssetRedirectMap, Jr as AssemblyReader, Jt as producerRegionsFromState, K as green, Kn as stringifyValue, Kr as canonicalizeRegion, Kt as requireConfigString, L as WARM_THROUGHPUT_MEMBERS, Ln as exportNamesCarriedFrom, Lr as warnDeprecatedNoPrefixCliFlag, Lt as coerceCfnBoolean, M as planOrphanAdoption, Mi as retryClassificationText, Mn as forceQuitRecoveryClause, Mr as resolveSkipPrefix, Mt as isSettledInstanceState, N as MULTI_REGION_RECREATE_BLOCKED_TYPES, Nn as formatLockExpiry, Nr as resolveStateBucketWithDefault, Nt as WAFv2WebACLProvider, O as unsupportedFinalSnapshotError, Oi as isThrottlingError, On as S3StateBackend, Or as getLegacyStateBucketName, Ot as isImpossibleEmptyStoredAttribute, P as isStatefulRecreateTargetForReplace, Pn as shellQuote, Pr as resolveStateBucketWithDefaultAndSource, Pt as normalizeAwsTagsToCfn, Q as isExportAliasCollision, Qn as AssetModeResolver, Qr as AwsClients, Qt as s3BucketDualStackDomainName, R as coerceWarmThroughput$1, Rn as hasReadableExportSet, Rr as CFN_TEMPLATE_BODY_LIMIT, Rt as coerceCfnInteger, S as PRE_DELETE_SNAPSHOT_TYPES, Sn as maskSecretsInText, Sr as stripControlChars, St as isTerminationProtectionPropagationError, T as createPreDeleteFinalSnapshot, Ti as withErrorHandling, Tn as redactSecretsForState, Tr as Synthesizer, Tt as cfnRefValueFromPhysicalId, U as bold, Un as orphansCarriedFrom, Ur as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, Ut as replayWarn, V as toFiniteNumber, Vn as isReadableBag, Vr as findLargeInlineResources, Vt as configStringRefusal, W as cyan, Wn as shouldRetainResource, Wr as expectedOwnerParam, Wt as requireConfigArray, X as collectPublishedOutputNames, Xn as loadPublishableAssetManifest, Xt as s3BucketArn, Y as collectDeclaredOutputNames, Yn as createAssetRedirectResolver, Yr as processStackMessages, Yt as withSharedDrainBudget, Z as exportAliasCollisionScrubWarning, Zn as rewriteTemplateAssetReferences, Zr as resolveBucketRegion, Zt as s3BucketDomainName, _ as isSecretBearingReferenceString, _i as ResourceUpdateNotSupportedError, _n as errorCauseChain, _r as partitionSensitiveEnv, _t as slowCcOperationTimeoutMs, a as DeploymentEventsStore, ai as CrossAccountSecretRefusalError, an as findSilentDropProperties, ar as readBootstrapMarkerBody, at as clearOnUpdateRemoval, b as bindingSkippedOutputs, bn as markSameGenerationBag, br as runDockerStreaming, bt as deleteSkipReason, c as replayFailedOperations, ci as DynamicReferenceRegionAmbiguousError, cn as DagBuilder, cr as buildDenyExternalAccessPolicy, ct as createMaskedRetryLogger, d as updatePartialReason, di as LocalStartServiceError, dn as STATE_SOURCED_CROSS_GENERATION_RULES, dr as describeDockerCapturedOutput, dt as beginCommandInterruptScope, ei as resetAwsClients, en as s3BucketWebsiteUrl, er as assertAssetBucketRegion, et as secretBearingStateKeyWarning, f as withResourceDeadline, fn as STATE_SOURCED_READBACK_RULES, fr as describeDockerExecFailure, ft as endCommandInterruptScope, g as bagHoldsSecretExpression, gi as ResourceTimeoutError, gn as dynamicReferenceTokens, gr as getDockerCmd, gt as CloudControlProvider, h as computeImplicitDeleteEdges, hi as ProvisioningError, hn as createSecretMasker, hr as formatDockerLoginError, ht as startInterruptWatch, i as DeploymentEventsReader, in as findActionableSilentDrops, ir as parseBootstrapMarker, it as collectInlinePolicyNamesManagedBySiblings, j as makeSiblingClaimReader, ji as markRedactedCause, jn as buildLockContentionMessage, jr as resolveCaptureObservedState, jt as refStateLookupFromResource, k as makeCanonicalizePropertiesFn, ki as isTransientServerError, kn as rebuildClientForBucketRegion, kr as resolveApp, kt as isUnboundTemplateParameter, l as replayRollback, li as IntrinsicResolutionRefusalError, ln as TemplateParser, lr as describeAwsFailure, lt as maskDeep, m as IMPLICIT_DELETE_DEPENDENCIES, mi as PartialFailureError, mn as carriesSecretMask, mr as dockerSpawnEnvWithSensitive, mt as isInterruptedWaitError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as DiffCalculator, nr as getBootstrapMarkerKey, nt as getCurrentResourceSecrets, o as planFailedOps, on as describeTypeWithThrottleRetry, or as validateAssetBucketName, ot as ProviderRegistry, p as maskingRetryLogger, pi as NestedStackChildDirectDestroyError, pn as TEMPLATE_SOURCED_RULES, pr as describeDockerFailure, pt as interruptWatchListenerCount, q as red, qn as WorkGraph, qr as derivePartitionAndUrlSuffix, qt as classifyReplaySecretRegion, r as DeployEngine, ri as CdkdError, rn as INTRINSIC_KEYS, rr as isCrossRegionRedirect, rt as IAMRoleProvider, s as planRollback, si as DeployCancelledError, sn as withRetry, sr as validateContainerRepoName, st as wouldReturnToSdkProvider, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as setAwsClients, tn as applyRoleArnIfSet, tr as ensureAssetStorage, tt as secretSafeKeyDisplay, u as updatePartialMessage, ui as LocalInvokeBuildError$1, un as STATE_SOURCED_BASELINE_RULES, ur as buildDockerImage, ut as maskerOrIdentity, v as keptWholeReasonText, vi as StackHasActiveImportsError, vn as identityKeyFor, vr as redactDockerArgvValues, vt as UNSPECIFIED_SKIP_REASON, w as ccRoutedFinalSnapshotError, wi as normalizeAwsError, wn as recoverMaskedOutput, wr as getDockerImageBySourceHash, wt as carriesDynamicReference, x as ATOMIC_FINAL_SNAPSHOT_TYPES, xn as maskSecretsInError, xr as escapeRegExp, xt as disableInstanceApiTermination, y as mergeNoChangeOutputs, yi as StackTerminationProtectionError, yn as isSingleDynamicReferenceToken, yr as runDockerForeground, yt as deleteIndeterminateGuards, z as isWarmThroughputDecrease, zn as importableOutputKeys, zr as CFN_TEMPLATE_URL_LIMIT, zt as configBooleanRefusal } from "./deploy-engine-v7h1H_Ac.js";
1
+ import { $ as secretBearing, $n as BOOTSTRAP_MARKER_PREFIX, $r as getAwsClients, $t as s3BucketRegionalDomainName, A as extractDeploymentEventError, Ai as markNonRetryable, An as buildForceUnlockCommand, Ar as resolveAutoAssetStorage, At as parameterTypeMayLoseSecretIdentity, B as toCfnInteger, Bn as importableOutputs, Br as MIGRATE_TMP_PREFIX, Bt as configIntegerRefusal, C as buildFinalSnapshotIdentifier, Cn as recordMaskOnlyValue, Cr as AssetManifestLoader, Ct as IntrinsicFunctionResolver, D as refusesFinalSnapshot, Di as isRetryableTransientError, Dn as LockManager, Dr as getDefaultStateBucketName, Dt as getAccountInfo, E as isFinalSnapshotError, Ei as isMarkedNonRetryable, En as scrubResourceRecord, Er as synthesisStatusMessage, Et as coerceParameterTypedValue, F as isStatefulRecreateTargetSync, Fn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Fr as resolveUseCdkBootstrapAssets, Ft as resolveExplicitPhysicalId, G as gray, Gn as AssetPublisher, Gr as PARTITION_TABLE, Gt as requireConfigObject, H as formatResourceLine, Hn as orphansAfterRollback, Hr as uploadCfnTemplate, Ht as readConfigString, I as renderStatefulReason, In as DEFAULT_STATE_PREFIX, Ir as stateBucketExistenceConfirmed, It as assertRegionMatch, J as yellow, Jn as buildAssetRedirectMap, Jr as AssemblyReader, Jt as producerRegionsFromState, K as green, Kn as stringifyValue, Kr as canonicalizeRegion, Kt as requireConfigString, L as WARM_THROUGHPUT_MEMBERS, Ln as exportNamesCarriedFrom, Lr as warnDeprecatedNoPrefixCliFlag, Lt as coerceCfnBoolean, M as planOrphanAdoption, Mi as retryClassificationText, Mn as forceQuitRecoveryClause, Mr as resolveSkipPrefix, Mt as isSettledInstanceState, N as MULTI_REGION_RECREATE_BLOCKED_TYPES, Nn as formatLockExpiry, Nr as resolveStateBucketWithDefault, Nt as WAFv2WebACLProvider, O as unsupportedFinalSnapshotError, Oi as isThrottlingError, On as S3StateBackend, Or as getLegacyStateBucketName, Ot as isImpossibleEmptyStoredAttribute, P as isStatefulRecreateTargetForReplace, Pn as shellQuote, Pr as resolveStateBucketWithDefaultAndSource, Pt as normalizeAwsTagsToCfn, Q as isExportAliasCollision, Qn as AssetModeResolver, Qr as AwsClients, Qt as s3BucketDualStackDomainName, R as coerceWarmThroughput$1, Rn as hasReadableExportSet, Rr as CFN_TEMPLATE_BODY_LIMIT, Rt as coerceCfnInteger, S as PRE_DELETE_SNAPSHOT_TYPES, Sn as maskSecretsInText, Sr as stripControlChars, St as isTerminationProtectionPropagationError, T as createPreDeleteFinalSnapshot, Ti as withErrorHandling, Tn as redactSecretsForState, Tr as Synthesizer, Tt as cfnRefValueFromPhysicalId, U as bold, Un as orphansCarriedFrom, Ur as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, Ut as replayWarn, V as toFiniteNumber, Vn as isReadableBag, Vr as findLargeInlineResources, Vt as configStringRefusal, W as cyan, Wn as shouldRetainResource, Wr as expectedOwnerParam, Wt as requireConfigArray, X as collectPublishedOutputNames, Xn as loadPublishableAssetManifest, Xt as s3BucketArn, Y as collectDeclaredOutputNames, Yn as createAssetRedirectResolver, Yr as processStackMessages, Yt as withSharedDrainBudget, Z as exportAliasCollisionScrubWarning, Zn as rewriteTemplateAssetReferences, Zr as resolveBucketRegion, Zt as s3BucketDomainName, _ as isSecretBearingReferenceString, _i as ResourceUpdateNotSupportedError, _n as errorCauseChain, _r as partitionSensitiveEnv, _t as slowCcOperationTimeoutMs, a as DeploymentEventsStore, ai as CrossAccountSecretRefusalError, an as findSilentDropProperties, ar as readBootstrapMarkerBody, at as clearOnUpdateRemoval, b as bindingSkippedOutputs, bn as markSameGenerationBag, br as runDockerStreaming, bt as deleteSkipReason, c as replayFailedOperations, ci as DynamicReferenceRegionAmbiguousError, cn as DagBuilder, cr as buildDenyExternalAccessPolicy, ct as createMaskedRetryLogger, d as updatePartialReason, di as LocalStartServiceError, dn as STATE_SOURCED_CROSS_GENERATION_RULES, dr as describeDockerCapturedOutput, dt as beginCommandInterruptScope, ei as resetAwsClients, en as s3BucketWebsiteUrl, er as assertAssetBucketRegion, et as secretBearingStateKeyWarning, f as withResourceDeadline, fn as STATE_SOURCED_READBACK_RULES, fr as describeDockerExecFailure, ft as endCommandInterruptScope, g as bagHoldsSecretExpression, gi as ResourceTimeoutError, gn as dynamicReferenceTokens, gr as getDockerCmd, gt as CloudControlProvider, h as computeImplicitDeleteEdges, hi as ProvisioningError, hn as createSecretMasker, hr as formatDockerLoginError, ht as startInterruptWatch, i as DeploymentEventsReader, in as findActionableSilentDrops, ir as parseBootstrapMarker, it as collectInlinePolicyNamesManagedBySiblings, j as makeSiblingClaimReader, ji as markRedactedCause, jn as buildLockContentionMessage, jr as resolveCaptureObservedState, jt as refStateLookupFromResource, k as makeCanonicalizePropertiesFn, ki as isTransientServerError, kn as rebuildClientForBucketRegion, kr as resolveApp, kt as isUnboundTemplateParameter, l as replayRollback, li as IntrinsicResolutionRefusalError, ln as TemplateParser, lr as describeAwsFailure, lt as maskDeep, m as IMPLICIT_DELETE_DEPENDENCIES, mi as PartialFailureError, mn as carriesSecretMask, mr as dockerSpawnEnvWithSensitive, mt as isInterruptedWaitError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as DiffCalculator, nr as getBootstrapMarkerKey, nt as getCurrentResourceSecrets, o as planFailedOps, on as describeTypeWithThrottleRetry, or as validateAssetBucketName, ot as ProviderRegistry, p as maskingRetryLogger, pi as NestedStackChildDirectDestroyError, pn as TEMPLATE_SOURCED_RULES, pr as describeDockerFailure, pt as interruptWatchListenerCount, q as red, qn as WorkGraph, qr as derivePartitionAndUrlSuffix, qt as classifyReplaySecretRegion, r as DeployEngine, ri as CdkdError, rn as INTRINSIC_KEYS, rr as isCrossRegionRedirect, rt as IAMRoleProvider, s as planRollback, si as DeployCancelledError, sn as withRetry, sr as validateContainerRepoName, st as wouldReturnToSdkProvider, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as setAwsClients, tn as applyRoleArnIfSet, tr as ensureAssetStorage, tt as secretSafeKeyDisplay, u as updatePartialMessage, ui as LocalInvokeBuildError$1, un as STATE_SOURCED_BASELINE_RULES, ur as buildDockerImage, ut as maskerOrIdentity, v as keptWholeReasonText, vi as StackHasActiveImportsError, vn as identityKeyFor, vr as redactDockerArgvValues, vt as UNSPECIFIED_SKIP_REASON, w as ccRoutedFinalSnapshotError, wi as normalizeAwsError, wn as recoverMaskedOutput, wr as getDockerImageBySourceHash, wt as carriesDynamicReference, x as ATOMIC_FINAL_SNAPSHOT_TYPES, xn as maskSecretsInError, xr as escapeRegExp, xt as disableInstanceApiTermination, y as mergeNoChangeOutputs, yi as StackTerminationProtectionError, yn as isSingleDynamicReferenceToken, yr as runDockerForeground, yt as deleteIndeterminateGuards, z as isWarmThroughputDecrease, zn as importableOutputKeys, zr as CFN_TEMPLATE_URL_LIMIT, zt as configBooleanRefusal } from "./deploy-engine-B91NMqm1.js";
2
2
  import { C as displaySafe, S as displayIdent, _ as withSkipPrefix, b as STACK_REF_MAX_CODE_POINTS, c as PATTERN_B_NAME_OPTIONS, f as explicitNamePropertyFor, h as getCurrentStackName, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as generateResourceNameWithFallback, n as getLogger, o as runStackBuffered, p as generateResourceName, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES, v as withStackName, w as truncateCodePoints, x as UNRENDERABLE } from "./logger-C17H06QA.js";
3
3
  import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
4
- import { t as getCdkdVersion } from "./version-CqnAWzcc.js";
5
- import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-BAYjqZuM.js";
4
+ import { t as getCdkdVersion } from "./version-1FTpsWNc.js";
5
+ import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-CvkcE8rH.js";
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
7
  import { createHash, randomBytes, randomUUID } from "node:crypto";
8
8
  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, NotFound, 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";
@@ -7079,6 +7079,46 @@ function isTruthyCfnBoolean(value) {
7079
7079
  //#endregion
7080
7080
  //#region src/provisioning/providers/s3-bucket-provider.ts
7081
7081
  /**
7082
+ * The "remove this bucket by hand" clause both partial-create cleanup arms end
7083
+ * with, rendered through the shared sanitize / shell-quote / SUPPRESS
7084
+ * (`renderDisableCommand`, the issue
7085
+ * [#2669](https://github.com/go-to-k/cdkd/issues/2669) shape; adopted here by
7086
+ * issue [#3136](https://github.com/go-to-k/cdkd/issues/3136)).
7087
+ *
7088
+ * `bucketName` is TEMPLATE-chosen — `Properties.BucketName`, or the name cdkd
7089
+ * generated for the resource — and until now both arms hand-quoted it as
7090
+ * `--bucket '<name>'`, so a `'` in the value broke out of the quoting and a
7091
+ * control byte forged lines on the operator's terminal. (A WARN, so that is
7092
+ * its whole reach — the recorder persists a thrown `error.message`, not this.)
7093
+ * When sanitizing CHANGES the name the whole command is
7094
+ * SUPPRESSED rather than shown with the sanitized value, because such a command
7095
+ * would delete a DIFFERENT bucket — the wrong-target harm the shared renderer
7096
+ * exists to prevent, and the reason emitting no command is the honest answer
7097
+ * rather than a fallback.
7098
+ *
7099
+ * ONE clause for both arms so the two cannot come to render the same value
7100
+ * differently. The prose `(${bucketName})` each arm carries BESIDE it is still
7101
+ * interpolated as-is: this clause takes only the pasteable half, and
7102
+ * display-sanitizing the PROSE of provider warnings is tracked as issue
7103
+ * [#3269](https://github.com/go-to-k/cdkd/issues/3269) (#3136 covers the
7104
+ * COMMAND half across providers, not prose).
7105
+ *
7106
+ * `maskSecrets` is REQUIRED rather than optional, and both call sites build it
7107
+ * from `context?.maskSecrets` (security review of #3136, which found these two
7108
+ * arms reaching no masker at all). It does two things: the arms now route
7109
+ * their whole warning through it, and it makes `renderDisableCommand` SUPPRESS
7110
+ * the command for a secret-bearing bucket name — which a message-level mask
7111
+ * could not catch once `shellQuote` has escaped an inner quote.
7112
+ */
7113
+ function manualBucketDeletionClause(bucketName, maskSecrets) {
7114
+ const command = renderDisableCommand({
7115
+ before: "aws s3api delete-bucket --bucket",
7116
+ identifier: bucketName,
7117
+ maskSecrets
7118
+ });
7119
+ return command ? `Manual deletion may be required before the next deploy: ${command}` : "Manual deletion may be required before the next deploy, via the console: the bucket name cannot be reproduced safely on a command line, so any command shown here would act on a different bucket.";
7120
+ }
7121
+ /**
7082
7122
  * A plain (non-array) object. `typeof x === 'object'` alone accepts arrays and
7083
7123
  * `null`; a `Transition: []` or an unresolved intrinsic pushed through as a
7084
7124
  * single transition entry makes S3 answer `MalformedXML` for the WHOLE
@@ -10440,11 +10480,13 @@ var S3BucketProvider = class S3BucketProvider {
10440
10480
  this.logger.debug(`Cleaned up partially-created S3 bucket ${logicalId} (${bucketName}) after wiring failure`);
10441
10481
  } catch (cleanupError) {
10442
10482
  this.logger.debug(`DeleteBucket cleanup failed for S3 bucket ${logicalId} (${bucketName}): ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
10443
- this.logger.warn(`Failed to clean up partially-created S3 bucket ${logicalId} (${bucketName}) (${cleanupError instanceof Error ? cleanupError.name : typeof cleanupError}). Re-run with --verbose for AWS's own message. Manual deletion may be required before the next deploy: aws s3api delete-bucket --bucket '${bucketName}'`);
10483
+ const maskSecrets = maskerOrIdentity(context?.maskSecrets);
10484
+ this.logger.warn(maskSecrets(`Failed to clean up partially-created S3 bucket ${logicalId} (${bucketName}) (${cleanupError instanceof Error ? cleanupError.name : typeof cleanupError}). Re-run with --verbose for AWS's own message. ` + manualBucketDeletionClause(bucketName, maskSecrets)));
10444
10485
  }
10445
10486
  else if (preflight.kind === "indeterminate") {
10446
10487
  this.logger.debug(`GetBucketLocation failed for S3 bucket ${bucketName} (${logicalId}) during create: ${preflight.reason}`);
10447
- this.logger.warn(`Not cleaning up S3 bucket ${logicalId} (${bucketName}) after a wiring failure: this deploy could not confirm whether it created the bucket (region probe failed: ${preflight.errorName}), and in us-east-1 a successful CreateBucket does not prove it. Re-run with --verbose for AWS's own message. If cdkd created it, delete it manually before the next deploy: aws s3api delete-bucket --bucket '${bucketName}'`);
10488
+ const maskSecrets = maskerOrIdentity(context?.maskSecrets);
10489
+ this.logger.warn(maskSecrets(`Not cleaning up S3 bucket ${logicalId} (${bucketName}) after a wiring failure: this deploy could not confirm whether it created the bucket (region probe failed: ${preflight.errorName}), and in us-east-1 a successful CreateBucket does not prove it. Re-run with --verbose for AWS's own message. If cdkd created it, the bucket is an orphan. ` + manualBucketDeletionClause(bucketName, maskSecrets)));
10448
10490
  }
10449
10491
  throw innerError;
10450
10492
  }
@@ -17881,16 +17923,39 @@ function mapSSESpecification(raw) {
17881
17923
  * the index to the same warn-and-omit path an absent block takes, letting AWS
17882
17924
  * name it — the CFn-handler outcome.
17883
17925
  *
17884
- * CFn is stringly typed, so a numeric string is accepted and coerced; `NaN` /
17885
- * `Infinity` / objects / unresolved intrinsics are not.
17926
+ * CFn is stringly typed, so a numeric string is accepted and coerced — through
17927
+ * {@link coerceCfnInteger}, CloudFormation's MEASURED DynamoDB Integer grammar
17928
+ * (issue [#3147](https://github.com/go-to-k/cdkd/issues/3147); the table is on
17929
+ * `toCfnInteger` in `../dynamodb-warm-throughput.ts`): an optional sign and
17930
+ * decimal digits, with NO trim, so `" 7 "` / `"7 "` / `""` / `"0x9"` / `"1e1"`
17931
+ * / `"6.5"` are every one `undefined` here because CloudFormation refuses the
17932
+ * template that spells them. The pre-#3147 reader was `Number()`, which
17933
+ * forwarded `7` / `9` / `10` for the first four and deployed an index at a
17934
+ * capacity CloudFormation would not have accepted. `NaN` / `Infinity` / a
17935
+ * non-integer number / objects / unresolved intrinsics were never usable and
17936
+ * still are not.
17886
17937
  */
17887
17938
  function readCapacityNumber(block, member) {
17888
- if (typeof block !== "object" || block === null || Array.isArray(block)) return void 0;
17889
- const raw = block[member];
17890
- if (raw === void 0 || raw === null || raw === "") return void 0;
17891
- if (typeof raw !== "number" && typeof raw !== "string") return void 0;
17892
- const value = Number(raw);
17893
- return Number.isFinite(value) ? value : void 0;
17939
+ if (!isPlainCapacityBlock(block)) return void 0;
17940
+ return coerceCfnInteger(block[member]);
17941
+ }
17942
+ /**
17943
+ * Is this a `ProvisionedThroughput` block whose MEMBERS can be read at all?
17944
+ *
17945
+ * One spelling, shared by {@link readCapacityNumber} — where a `false` answer
17946
+ * means "no usable member" — and by
17947
+ * {@link DynamoDBTableProvider.coerceIndexCapacityBlock}, where it means
17948
+ * "forward this VERBATIM and let AWS name the shape" (issue
17949
+ * [#3255](https://github.com/go-to-k/cdkd/issues/3255)). The two conclusions
17950
+ * differ; the shape test must not, or the create forwarder could rewrite a
17951
+ * block the flip's reader refuses to read (or vice versa).
17952
+ *
17953
+ * An ARRAY is excluded deliberately: `[]` indexes to `undefined` for both
17954
+ * members, so treating it as a block would DROP nothing and report nothing
17955
+ * while `CreateTable` rejects the list by name anyway.
17956
+ */
17957
+ function isPlainCapacityBlock(value) {
17958
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17894
17959
  }
17895
17960
  /**
17896
17961
  * Whether a desired `GlobalSecondaryIndexes[]` entry declares BOTH capacity
@@ -17901,19 +17966,51 @@ function readCapacityNumber(block, member) {
17901
17966
  * BOTH members are required, exactly as in the flip block: an entry declaring
17902
17967
  * only one is treated like an absent one there, so accepting it here would let
17903
17968
  * the pre-flip delete run ahead of a flip AWS still rejects.
17969
+ *
17970
+ * A MIRROR, so it calls {@link readCapacityNumber} — the function the flip's
17971
+ * per-index forwarder calls — rather than restating the rule. That is what kept
17972
+ * it correct across issue [#3147](https://github.com/go-to-k/cdkd/issues/3147):
17973
+ * the reader moved to CloudFormation's Integer grammar and this predicate moved
17974
+ * with it, so a padded `" 7 "` is "no usable declared capacity" here exactly
17975
+ * because the forwarder drops it there.
17904
17976
  */
17905
17977
  function hasUsableDeclaredCapacity(entry) {
17906
17978
  const declared = entry?.["ProvisionedThroughput"];
17907
17979
  return readCapacityNumber(declared, "ReadCapacityUnits") !== void 0 && readCapacityNumber(declared, "WriteCapacityUnits") !== void 0;
17908
17980
  }
17909
17981
  /**
17982
+ * The two `ProvisionedThroughput` members, in the order the forwarders build
17983
+ * them. Named once so {@link hasUsableTableCapacity}, {@link tableCapacityForSend}
17984
+ * and the per-INDEX create forwarder
17985
+ * ({@link DynamoDBTableProvider.coerceIndexCapacityBlock}) cannot come to
17986
+ * disagree about the membership.
17987
+ *
17988
+ * Deliberately NOT `TABLE_CAPACITY_MEMBERS` any more (issue
17989
+ * [#3255](https://github.com/go-to-k/cdkd/issues/3255)): CloudFormation's
17990
+ * `ProvisionedThroughput` block has the same two members at the TABLE level and
17991
+ * inside every `GlobalSecondaryIndexes[]` entry, and a second list for the
17992
+ * per-index reader would be exactly the drift this constant exists to prevent.
17993
+ * What differs between the two levels is what an ABSENT member does — the table
17994
+ * level substitutes {@link DEFAULT_TABLE_CAPACITY_UNITS}, the per-index level
17995
+ * deliberately has no default ({@link readCapacityNumber}) — and that is a
17996
+ * property of each forwarder, not of the membership.
17997
+ */
17998
+ const PROVISIONED_CAPACITY_MEMBERS = ["ReadCapacityUnits", "WriteCapacityUnits"];
17999
+ /**
18000
+ * The capacity both TABLE-level forwarders substitute for a member the
18001
+ * template does not declare at all — `create()`'s `CreateTable` and the
18002
+ * BillingMode flip's `UpdateTable`. It is the pre-issue-#3147 `?? 5`, kept
18003
+ * BYTE-identical in behaviour for the absent case; what changed is only what a
18004
+ * DECLARED-but-unusable member does.
18005
+ */
18006
+ const DEFAULT_TABLE_CAPACITY_UNITS = 5;
18007
+ /**
17910
18008
  * Whether the desired TABLE-level `ProvisionedThroughput` is one the flip can
17911
18009
  * actually send (issue #1617 PR review).
17912
18010
  *
17913
18011
  * Deliberately NOT {@link hasUsableDeclaredCapacity}: the flip defaults each
17914
- * member to 5 when absent (`Number(pt['ReadCapacityUnits'] ?? 5)`), so a
17915
- * half-declared TABLE capacity is sendable while a half-declared per-INDEX one
17916
- * is not.
18012
+ * ABSENT member to 5 ({@link tableCapacityForSend}), so a half-declared TABLE
18013
+ * capacity is sendable while a half-declared per-INDEX one is not.
17917
18014
  *
17918
18015
  * It mirrors the flip's GATE as well as its arithmetic, which the first version
17919
18016
  * did not and a second review round caught — both halves of that miss are real:
@@ -17921,23 +18018,78 @@ function hasUsableDeclaredCapacity(entry) {
17921
18018
  * - the gate is plain TRUTHINESS (`properties['ProvisionedThroughput']`), so a
17922
18019
  * non-object truthy value sends `{5, 5}` and SUCCEEDS. Requiring a plain
17923
18020
  * object refused it and skipped a removal that would have been fine.
17924
- * - a DECLARED member that coerces to `0` (`''`, `false`, `[]`) is finite, so
17925
- * an arithmetic-only check accepted it — while AWS rejects a capacity below
17926
- * 1, which is precisely the deterministic rejection this predicate exists to
17927
- * keep a delete from running ahead of.
18021
+ * - a DECLARED member that coerces to `0` (`0`, `'0'`) is a capacity AWS
18022
+ * rejects (below 1), which is precisely the deterministic rejection this
18023
+ * predicate exists to keep a delete from running ahead of.
18024
+ *
18025
+ * It reads through {@link tableCapacityForSend}'s OWN reader since issue
18026
+ * [#3147](https://github.com/go-to-k/cdkd/issues/3147) — `coerceCfnInteger`,
18027
+ * plus the `< 1` rule AWS adds on top of the grammar — so the two cannot
18028
+ * answer differently. On the pre-#3147 `Number()` reader they DID, in both
18029
+ * directions: a padded `" 7 "` counted as usable here while the forwarder now
18030
+ * drops it, and a DECLARED `null` counted as UNUSABLE here while the
18031
+ * forwarder's `?? 5` sent 5 and AWS accepted the flip — so this predicate
18032
+ * refused a pre-flip removal on a flip that would have succeeded, warning that
18033
+ * "AWS rejects the flip either way" when it does not. `null` therefore takes
18034
+ * the ABSENT arm here, exactly as it does at the forwarder; `''` / `false` /
18035
+ * `[]`, which `Number()` read as **0**, are now dropped-and-named by the
18036
+ * forwarder, so they make the flip fail and stay UNUSABLE here.
17928
18037
  */
17929
18038
  function hasUsableTableCapacity(value) {
17930
18039
  if (!value) return false;
17931
18040
  if (typeof value !== "object" || Array.isArray(value)) return true;
17932
18041
  const pt = value;
17933
- for (const member of ["ReadCapacityUnits", "WriteCapacityUnits"]) {
17934
- if (pt[member] === void 0) continue;
17935
- const n = Number(pt[member]);
17936
- if (!Number.isFinite(n) || n < 1) return false;
18042
+ for (const member of PROVISIONED_CAPACITY_MEMBERS) {
18043
+ const raw = pt[member];
18044
+ if (raw === void 0 || raw === null) continue;
18045
+ const n = coerceCfnInteger(raw);
18046
+ if (n === void 0 || n < 1) return false;
17937
18047
  }
17938
18048
  return true;
17939
18049
  }
17940
18050
  /**
18051
+ * Read one TABLE-level `ProvisionedThroughput` member for the wire (issue
18052
+ * [#3147](https://github.com/go-to-k/cdkd/issues/3147)).
18053
+ *
18054
+ * THREE answers, where the pre-#3147 `Number(pt[member] ?? 5)` had two:
18055
+ *
18056
+ * - ABSENT (`undefined` / `null`, matching `??`) -> the
18057
+ * {@link DEFAULT_TABLE_CAPACITY_UNITS} default, silently, exactly as before.
18058
+ * - a value {@link coerceCfnInteger} accepts -> that integer.
18059
+ * - anything else -> `undefined`, i.e. the member is OMITTED from the request,
18060
+ * and `onUnusable` announces it.
18061
+ *
18062
+ * The third answer is the decision this function exists for, and it is NOT the
18063
+ * announced-default one its `AWS::DynamoDB::GlobalTable` sibling takes. Two
18064
+ * reasons, both local to this type:
18065
+ *
18066
+ * - **This file's own precedent for the table-level capacity is "let AWS
18067
+ * reject it, by name".** A flip to PROVISIONED carrying NO
18068
+ * `ProvisionedThroughput` at all is deliberately left to fail at AWS as CFn
18069
+ * parity (see the comment at that gate), and the per-index arm below refuses
18070
+ * to invent a capacity in even stronger terms. Substituting 5 for a
18071
+ * DECLARED `" 7 "` would be the one place this type guesses.
18072
+ * - **A substituted capacity is invisible afterwards.** State records the
18073
+ * DESIRED bag, so a table created at 5 against a recorded `" 7 "` compares
18074
+ * EQUAL on every later `cdkd diff`: the divergence never surfaces and the
18075
+ * warning scrolls past once. Omitting the member fails the deploy with
18076
+ * DynamoDB naming the member, which is also what CloudFormation does with
18077
+ * this template (it refuses it at properties validation).
18078
+ *
18079
+ * `onUnusable` is called at most once per member and never for the absent case,
18080
+ * so an ordinary template is silent.
18081
+ */
18082
+ function tableCapacityForSend(pt, member, onUnusable) {
18083
+ const raw = pt?.[member];
18084
+ if (raw === void 0 || raw === null) return DEFAULT_TABLE_CAPACITY_UNITS;
18085
+ const coerced = coerceCfnInteger(raw);
18086
+ if (coerced === void 0) {
18087
+ onUnusable(member, raw);
18088
+ return;
18089
+ }
18090
+ return coerced;
18091
+ }
18092
+ /**
17941
18093
  * Does the capacity AWS currently holds for a GSI already equal the pair the
17942
18094
  * template requests? (issue #1630)
17943
18095
  *
@@ -18294,7 +18446,7 @@ function reverseMapSecondaryIndex(live, desired, bagInformative) {
18294
18446
  * predicate EVERY per-index write path calls — the `Create` / `Update` GSI
18295
18447
  * actions in `applyGsiUpdates` AND `create()`'s `CreateTable` forward, which
18296
18448
  * maps each entry through
18297
- * {@link DynamoDBTableProvider.coerceIndexWarmThroughputForCreate}. It is
18449
+ * {@link DynamoDBTableProvider.coerceIndexThroughputForCreate}. It is
18298
18450
  * TIGHTER than truthiness (at least one member resolving to a finite
18299
18451
  * number), so `WarmThroughput: {}` is neither sent nor emitted on any path.
18300
18452
  * An earlier version of this bullet carved out the TABLE-create path as an
@@ -18303,9 +18455,15 @@ function reverseMapSecondaryIndex(live, desired, bagInformative) {
18303
18455
  * template failed on a fresh create and succeeded on a later GSI add. Fixed
18304
18456
  * in PR review round 6; the carve-out is gone because the divergence is.
18305
18457
  * - `ProvisionedThroughput` / `OnDemandThroughput` keep TRUTHINESS, which is
18306
- * what their write path does: those two members ARE still forwarded verbatim
18307
- * (the create-path mapper rewrites only `WarmThroughput`), and
18308
- * `applyGsiUpdates` gates on `gsi.ProvisionedThroughput` alone. Widening
18458
+ * still what their write path does: `applyGsiUpdates` gates on
18459
+ * `gsi.ProvisionedThroughput` alone, `OnDemandThroughput` is forwarded
18460
+ * verbatim on every path, and the `ProvisionedThroughput` coercion issue
18461
+ * [#3255](https://github.com/go-to-k/cdkd/issues/3255) added to ALL FOUR
18462
+ * per-index send sites (`create()`'s mapper plus `applyGsiUpdates`'
18463
+ * adopted-repair, `Create` and same-name `Update` actions) never
18464
+ * SUPPRESSES the block — it rewrites members inside a block that is sent
18465
+ * either way, so "would cdkd send this block" is unchanged by it, even
18466
+ * where both members were dropped and an empty block goes out. Widening
18309
18467
  * `isSendableWarmThroughput` over them would be wrong twice over — it reads
18310
18468
  * members those blocks do not have (`ReadUnitsPerSecond` vs
18311
18469
  * `ReadCapacityUnits`), so every declared capacity would read as undeclared.
@@ -18497,9 +18655,10 @@ var DynamoDBTableProvider = class {
18497
18655
  };
18498
18656
  if (billingMode === "PROVISIONED") {
18499
18657
  const pt = properties["ProvisionedThroughput"];
18658
+ const onUnusable = (member, raw) => this.warnUnusableProvisionedCapacity(`AWS::DynamoDB::Table ${logicalId}`, member, raw, maskSecrets);
18500
18659
  createParams.ProvisionedThroughput = {
18501
- ReadCapacityUnits: Number(pt?.["ReadCapacityUnits"] ?? 5),
18502
- WriteCapacityUnits: Number(pt?.["WriteCapacityUnits"] ?? 5)
18660
+ ReadCapacityUnits: tableCapacityForSend(pt, "ReadCapacityUnits", onUnusable),
18661
+ WriteCapacityUnits: tableCapacityForSend(pt, "WriteCapacityUnits", onUnusable)
18503
18662
  };
18504
18663
  }
18505
18664
  if (properties["OnDemandThroughput"]) createParams.OnDemandThroughput = properties["OnDemandThroughput"];
@@ -18512,7 +18671,7 @@ var DynamoDBTableProvider = class {
18512
18671
  };
18513
18672
  if (properties["GlobalSecondaryIndexes"]) {
18514
18673
  const declaredGsis = properties["GlobalSecondaryIndexes"];
18515
- createParams.GlobalSecondaryIndexes = Array.isArray(declaredGsis) ? declaredGsis.map((entry) => this.coerceIndexWarmThroughputForCreate(logicalId, entry, maskSecrets)) : declaredGsis;
18674
+ createParams.GlobalSecondaryIndexes = Array.isArray(declaredGsis) ? declaredGsis.map((entry) => this.coerceIndexThroughputForCreate(logicalId, entry, maskSecrets)) : declaredGsis;
18516
18675
  }
18517
18676
  if (properties["LocalSecondaryIndexes"]) createParams.LocalSecondaryIndexes = properties["LocalSecondaryIndexes"];
18518
18677
  const sse = mapSSESpecification(properties["SSESpecification"]);
@@ -18526,7 +18685,7 @@ var DynamoDBTableProvider = class {
18526
18685
  tableCreated = true;
18527
18686
  this.logger.debug(`CreateTable initiated for ${tableName}, waiting for ACTIVE status`);
18528
18687
  const tableInfo = await this.waitForTableActive(tableName);
18529
- await this.applyPointInTimeRecovery(tableName, properties["PointInTimeRecoverySpecification"]);
18688
+ await this.applyPointInTimeRecovery(tableName, properties["PointInTimeRecoverySpecification"], void 0, maskSecrets);
18530
18689
  await this.applyTimeToLive(tableName, properties["TimeToLiveSpecification"]);
18531
18690
  await this.applyKinesisStreamingDestination(tableName, properties["KinesisStreamSpecification"], void 0, maskSecrets);
18532
18691
  await this.applyContributorInsights(tableName, properties["ContributorInsightsSpecification"]);
@@ -18630,9 +18789,10 @@ var DynamoDBTableProvider = class {
18630
18789
  if (tableClassChanged) updateInput.TableClass = normalizeTableClass(properties["TableClass"]);
18631
18790
  if (billingOrThroughputChanged && billingMode !== "PAY_PER_REQUEST" && properties["ProvisionedThroughput"]) {
18632
18791
  const pt = properties["ProvisionedThroughput"];
18792
+ const onUnusable = (member, raw) => this.warnUnusableProvisionedCapacity(`AWS::DynamoDB::Table ${logicalId}`, member, raw, maskSecrets);
18633
18793
  updateInput.ProvisionedThroughput = {
18634
- ReadCapacityUnits: Number(pt["ReadCapacityUnits"] ?? 5),
18635
- WriteCapacityUnits: Number(pt["WriteCapacityUnits"] ?? 5)
18794
+ ReadCapacityUnits: tableCapacityForSend(pt, "ReadCapacityUnits", onUnusable),
18795
+ WriteCapacityUnits: tableCapacityForSend(pt, "WriteCapacityUnits", onUnusable)
18636
18796
  };
18637
18797
  }
18638
18798
  const flipLiveIndexes = liveIndexes.filter((live) => typeof live.IndexName !== "string" || !preFlipDeletedIndexNames.has(live.IndexName));
@@ -18740,7 +18900,7 @@ var DynamoDBTableProvider = class {
18740
18900
  }
18741
18901
  }
18742
18902
  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);
18743
- if (JSON.stringify(properties["PointInTimeRecoverySpecification"]) !== JSON.stringify(previousProperties["PointInTimeRecoverySpecification"])) await this.applyPointInTimeRecovery(physicalId, properties["PointInTimeRecoverySpecification"], previousProperties["PointInTimeRecoverySpecification"]);
18903
+ if (JSON.stringify(properties["PointInTimeRecoverySpecification"]) !== JSON.stringify(previousProperties["PointInTimeRecoverySpecification"])) await this.applyPointInTimeRecovery(physicalId, properties["PointInTimeRecoverySpecification"], previousProperties["PointInTimeRecoverySpecification"], maskSecrets);
18744
18904
  if (JSON.stringify(properties["TimeToLiveSpecification"]) !== JSON.stringify(previousProperties["TimeToLiveSpecification"])) {
18745
18905
  this.assertNoActiveTtlAttributeNameChange(logicalId, resourceType, physicalId, properties["TimeToLiveSpecification"], previousProperties["TimeToLiveSpecification"]);
18746
18906
  await this.applyTimeToLive(physicalId, properties["TimeToLiveSpecification"], previousProperties["TimeToLiveSpecification"]);
@@ -18933,13 +19093,20 @@ var DynamoDBTableProvider = class {
18933
19093
  * change", so a dropped block must be turned into an explicit
18934
19094
  * `PointInTimeRecoveryEnabled: false`).
18935
19095
  */
18936
- async applyPointInTimeRecovery(tableName, spec, previousSpec) {
19096
+ async applyPointInTimeRecovery(tableName, spec, previousSpec, maskSecrets = (text) => text) {
18937
19097
  let enabled;
18938
19098
  let recoveryPeriodInDays;
18939
19099
  if (spec !== void 0 && spec !== null) {
18940
19100
  const s = spec;
18941
19101
  enabled = Boolean(s["PointInTimeRecoveryEnabled"]);
18942
- if (enabled && s["RecoveryPeriodInDays"] !== void 0) recoveryPeriodInDays = Number(s["RecoveryPeriodInDays"]);
19102
+ if (enabled && s["RecoveryPeriodInDays"] !== void 0) {
19103
+ const rawRecoveryPeriod = s["RecoveryPeriodInDays"];
19104
+ recoveryPeriodInDays = coerceCfnInteger(rawRecoveryPeriod);
19105
+ if (recoveryPeriodInDays === void 0) {
19106
+ const warn = (message) => this.logger.warn(maskSecrets(message));
19107
+ warn(`DynamoDB table ${tableName}: PointInTimeRecoverySpecification.RecoveryPeriodInDays ${JSON.stringify(maskLeafValue$1(rawRecoveryPeriod, maskSecrets))} is not an Integer CloudFormation accepts (an optional sign and decimal digits only — no surrounding whitespace, hex, exponent or decimal point), so it was DROPPED from the UpdateContinuousBackups request: point-in-time recovery is enabled with DynamoDB's default recovery period instead of the declared one. Check for an unresolved intrinsic, or spell the value as a decimal integer.`);
19108
+ }
19109
+ }
18943
19110
  } else if (previousSpec !== void 0 && previousSpec !== null) enabled = false;
18944
19111
  if (enabled === void 0) return;
18945
19112
  const pitrSpec = { PointInTimeRecoveryEnabled: enabled };
@@ -19314,7 +19481,7 @@ var DynamoDBTableProvider = class {
19314
19481
  const adopted = { IndexName: name };
19315
19482
  let adoptedHasMember = false;
19316
19483
  if (liveCapacityComparable && gsi.ProvisionedThroughput && !handledByBillingFlip.has(name) && !liveCapacityAlreadyMatches(live.ProvisionedThroughput, gsi.ProvisionedThroughput) && !this.skipZeroCapacityIndexUpdate(name, physicalId, gsi.ProvisionedThroughput, maskSecrets)) {
19317
- adopted.ProvisionedThroughput = gsi.ProvisionedThroughput;
19484
+ adopted.ProvisionedThroughput = this.indexCapacityForSend(this.indexScopeAt(name, physicalId, maskSecrets), gsi.ProvisionedThroughput, maskSecrets);
19318
19485
  adoptedHasMember = true;
19319
19486
  }
19320
19487
  const adoptedWarm = this.warmThroughputOpFor(name, physicalId, gsi, void 0, live, maskSecrets);
@@ -19326,13 +19493,13 @@ var DynamoDBTableProvider = class {
19326
19493
  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.`);
19327
19494
  continue;
19328
19495
  }
19329
- this.warnRefusedWarmThroughput(`GSI ${maskSecrets(name)} on DynamoDB table ${physicalId}`, gsi.WarmThroughput, maskSecrets);
19330
- const createWarm = this.coerceWarmThroughputForSend(`GSI ${maskSecrets(name)} on DynamoDB table ${physicalId}`, gsi.WarmThroughput, maskSecrets);
19496
+ this.warnRefusedWarmThroughput(this.indexScopeAt(name, physicalId, maskSecrets), gsi.WarmThroughput, maskSecrets);
19497
+ const createWarm = this.coerceWarmThroughputForSend(this.indexScopeAt(name, physicalId, maskSecrets), gsi.WarmThroughput, maskSecrets);
19331
19498
  ops.push({ Create: {
19332
19499
  IndexName: name,
19333
19500
  KeySchema: gsi.KeySchema,
19334
19501
  Projection: gsi.Projection,
19335
- ...gsi.ProvisionedThroughput ? { ProvisionedThroughput: gsi.ProvisionedThroughput } : {},
19502
+ ...gsi.ProvisionedThroughput ? { ProvisionedThroughput: this.indexCapacityForSend(this.indexScopeAt(name, physicalId, maskSecrets), gsi.ProvisionedThroughput, maskSecrets) } : {},
19336
19503
  ...gsi.OnDemandThroughput ? { OnDemandThroughput: gsi.OnDemandThroughput } : {},
19337
19504
  ...createWarm ? { WarmThroughput: createWarm } : {}
19338
19505
  } });
@@ -19344,7 +19511,7 @@ var DynamoDBTableProvider = class {
19344
19511
  if (gsi.ProvisionedThroughput && !handledByBillingFlip.has(name) && JSON.stringify(before.ProvisionedThroughput) !== JSON.stringify(gsi.ProvisionedThroughput)) {
19345
19512
  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`);
19346
19513
  else if (!this.skipZeroCapacityIndexUpdate(name, physicalId, gsi.ProvisionedThroughput, maskSecrets)) {
19347
- update.ProvisionedThroughput = gsi.ProvisionedThroughput;
19514
+ update.ProvisionedThroughput = this.indexCapacityForSend(this.indexScopeAt(name, physicalId, maskSecrets), gsi.ProvisionedThroughput, maskSecrets);
19348
19515
  updateHasMember = true;
19349
19516
  }
19350
19517
  }
@@ -19376,35 +19543,211 @@ var DynamoDBTableProvider = class {
19376
19543
  return coerced.spec;
19377
19544
  }
19378
19545
  /**
19379
- * One `CreateTable` index entry with its `WarmThroughput` coerced (PR review
19380
- * round 6).
19546
+ * `GSI <name> on AWS::DynamoDB::Table <logicalId>` — the scope every
19547
+ * create-path per-index warning interpolates.
19548
+ *
19549
+ * The index NAME is a RESOLVED property value, so it is masked before it
19550
+ * becomes part of a message (issue #1997). It is also an UNCHECKED cast off
19551
+ * the template: `IndexName: 2024` (unquoted YAML) reaches here as a NUMBER,
19552
+ * and the real masker is a `String.prototype.replace` call that THROWS on
19553
+ * one — which would take the whole deploy down from a diagnostic path. So
19554
+ * the type test is the guard, not merely `!== undefined`: anything that is
19555
+ * not a string has no name to print and takes the `<unnamed>` arm, exactly
19556
+ * as an absent one does. (Reached by the review of issue
19557
+ * [#3255](https://github.com/go-to-k/cdkd/issues/3255): before it, the scope
19558
+ * was built only for an entry declaring `WarmThroughput`, so the crash was
19559
+ * unreachable for an ordinary PROVISIONED template.)
19560
+ */
19561
+ indexScope(indexName, logicalId, maskSecrets) {
19562
+ return `GSI ${typeof indexName === "string" ? maskSecrets(indexName) : "<unnamed>"} on AWS::DynamoDB::Table ${logicalId}`;
19563
+ }
19564
+ /**
19565
+ * `GSI <name> on DynamoDB table <physicalId>` — {@link indexScope}'s twin for
19566
+ * the UPDATE path, which names the LIVE table rather than the template's
19567
+ * logical id.
19568
+ *
19569
+ * Same guard, same reason, and the reason is why this exists at all rather
19570
+ * than the five call sites spelling `maskSecrets(name)` inline: `name` is a
19571
+ * key of a map built from `g.IndexName!` off an UNCHECKED template cast, so
19572
+ * `IndexName: 2024` (unquoted YAML) is truthy, becomes the key, and arrives
19573
+ * here as a NUMBER — on which the real masker's `String.prototype.replace`
19574
+ * THROWS. The create path was guarded when {@link indexScope} was added
19575
+ * (the review of issue #3255); the update sites that now build a scope
19576
+ * EAGERLY for the capacity coercion were not, which newly exposed the same
19577
+ * crash on an ordinary PROVISIONED capacity update with a non-empty secret
19578
+ * bag (found by the review of that round).
19579
+ */
19580
+ indexScopeAt(indexName, physicalId, maskSecrets) {
19581
+ return `GSI ${typeof indexName === "string" ? maskSecrets(indexName) : "<unnamed>"} on DynamoDB table ${physicalId}`;
19582
+ }
19583
+ /**
19584
+ * The per-index `ProvisionedThroughput` an `UpdateTable` action should put on
19585
+ * the wire (the review of issue
19586
+ * [#3255](https://github.com/go-to-k/cdkd/issues/3255)).
19587
+ *
19588
+ * Exists because coercing only `create()`'s forward left the file
19589
+ * INTERNALLY divergent in the direction the issue is about: the same index
19590
+ * entry succeeded on a fresh create and was rejected by AWS when added by a
19591
+ * later update, which is the `WarmThroughput` round-6 story with the
19592
+ * polarity flipped. All three `applyGsiUpdates` send sites — the adopted
19593
+ * index's repair `Update`, the `Create` action and the same-name `Update`
19594
+ * action — now read through {@link coerceIndexCapacityBlock}, the same
19595
+ * reader `create()` uses.
19596
+ *
19597
+ * **The GUARDS around those sites deliberately stay on `toFiniteNumber`**,
19598
+ * and that is the split `dynamodb-warm-throughput.ts`'s header already
19599
+ * records: a FORWARDER reads CloudFormation's grammar, a GUARD reads the
19600
+ * wider one, because over-acceptance is the safe side of an
19601
+ * already-matches / zero-capacity / change-detection test. Nothing moves as
19602
+ * a result — `liveCapacityAlreadyMatches` and `skipZeroCapacityIndexUpdate`
19603
+ * are asked about the DECLARED block exactly as before, and the change
19604
+ * detector still compares the declared block against the recorded one, both
19605
+ * raw. Only what reaches AWS changes.
19606
+ */
19607
+ indexCapacityForSend(scope, declared, maskSecrets) {
19608
+ if (declared === void 0) return void 0;
19609
+ return this.coerceIndexCapacityBlock(scope, declared, maskSecrets) ?? declared;
19610
+ }
19611
+ /**
19612
+ * One `CreateTable` index entry with BOTH of its throughput blocks coerced —
19613
+ * `WarmThroughput` (PR review round 6) and `ProvisionedThroughput` (issue
19614
+ * [#3255](https://github.com/go-to-k/cdkd/issues/3255)).
19381
19615
  *
19382
19616
  * `create()` forwards the declared `GlobalSecondaryIndexes` array to
19383
- * `CreateTable`, so it is a SEND SITE for the same per-index value the
19384
- * update path coerces — and it was the one the round-5 sweep missed. Entries
19385
- * are rebuilt rather than mutated: the input bag belongs to the caller (the
19386
- * resolved template, which the engine also records into state), and a
19387
- * provider that edits it in place would change what state reports cdkd sent.
19388
- *
19389
- * An entry that is not a plain object, or that declares no `WarmThroughput`,
19390
- * is returned UNCHANGED — including the identity of the object, so the
19391
- * common case allocates nothing.
19392
- */
19393
- coerceIndexWarmThroughputForCreate(logicalId, entry, maskSecrets = (text) => text) {
19617
+ * `CreateTable`, so it is a SEND SITE for the same per-index values the
19618
+ * update path coerces. `WarmThroughput` was the one the round-5 sweep
19619
+ * missed; `ProvisionedThroughput` was missed by round 6 AND by issue #3147's
19620
+ * own enumeration, because this forwarder is a CAST rather than a call, so
19621
+ * neither sweep's grep could see it. The BillingMode flip's per-index reader
19622
+ * ({@link readCapacityNumber}) has been on the grammar since #3147, so until
19623
+ * now create and flip disagreed about the same property: a CFn-legal
19624
+ * `ReadCapacityUnits: '7'` reached `CreateTable` as a STRING in a `number`
19625
+ * field, and a `' 7 '` CloudFormation refuses the template for was forwarded
19626
+ * unchanged.
19627
+ *
19628
+ * Entries are rebuilt rather than mutated: the input bag belongs to the
19629
+ * caller (the resolved template, which the engine also records into state),
19630
+ * and a provider that edits it in place would change what state reports cdkd
19631
+ * sent.
19632
+ *
19633
+ * An entry that is not a plain object, and one declaring NEITHER throughput
19634
+ * block, is returned UNCHANGED — identity included. An entry that DOES
19635
+ * declare one is rebuilt whenever that block's coercion changes anything;
19636
+ * the `WarmThroughput` half rebuilds even for an already-numeric value,
19637
+ * which is pre-existing behaviour and costs one spread per declared block.
19638
+ */
19639
+ coerceIndexThroughputForCreate(logicalId, entry, maskSecrets = (text) => text) {
19394
19640
  if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
19395
- const declared = entry["WarmThroughput"];
19396
- if (declared === void 0) return entry;
19397
- const scope = `GSI ${entry.IndexName === void 0 ? "<unnamed>" : maskSecrets(entry.IndexName)} on AWS::DynamoDB::Table ${logicalId}`;
19398
- const coerced = this.coerceWarmThroughputForSend(scope, declared, maskSecrets);
19399
- if (coerced === void 0) {
19400
- this.warnRefusedWarmThroughput(scope, declared, maskSecrets);
19401
- const { WarmThroughput: _dropped, ...rest } = entry;
19402
- return rest;
19641
+ const bag = entry;
19642
+ const declaredWarm = bag["WarmThroughput"];
19643
+ const declaredCapacity = bag["ProvisionedThroughput"];
19644
+ if (declaredWarm === void 0 && !isPlainCapacityBlock(declaredCapacity)) return entry;
19645
+ const scope = this.indexScope(entry.IndexName, logicalId, maskSecrets);
19646
+ let out = entry;
19647
+ if (declaredWarm !== void 0) {
19648
+ const coerced = this.coerceWarmThroughputForSend(scope, declaredWarm, maskSecrets);
19649
+ if (coerced === void 0) {
19650
+ this.warnRefusedWarmThroughput(scope, declaredWarm, maskSecrets);
19651
+ const { WarmThroughput: _dropped, ...rest } = out;
19652
+ out = rest;
19653
+ } else out = {
19654
+ ...out,
19655
+ WarmThroughput: coerced
19656
+ };
19403
19657
  }
19404
- return {
19405
- ...entry,
19406
- WarmThroughput: coerced
19658
+ const capacity = this.coerceIndexCapacityBlock(scope, declaredCapacity, maskSecrets);
19659
+ if (capacity !== void 0) out = {
19660
+ ...out,
19661
+ ProvisionedThroughput: capacity
19407
19662
  };
19663
+ return out;
19664
+ }
19665
+ /**
19666
+ * One `CreateTable` index entry's `ProvisionedThroughput`, with each DECLARED
19667
+ * member read through CloudFormation's DynamoDB Integer grammar (issue
19668
+ * [#3255](https://github.com/go-to-k/cdkd/issues/3255)). Returns `undefined`
19669
+ * when the block needs no rewriting at all.
19670
+ *
19671
+ * It reads through {@link readCapacityNumber} — the function the BillingMode
19672
+ * flip's per-index forwarder calls — rather than restating the grammar, for
19673
+ * the same reason {@link hasUsableDeclaredCapacity} does: that is what keeps
19674
+ * create and flip answering identically about one template value.
19675
+ *
19676
+ * THREE decisions, each of which had a plausible alternative:
19677
+ *
19678
+ * - **A rejected spelling drops the MEMBER, not the whole block.** This is
19679
+ * what {@link tableCapacityForSend} already does one level up for the
19680
+ * identical grammar, so create's two capacity forwarders answer the same
19681
+ * way; it keeps a sibling member the template spelled LEGALLY, which
19682
+ * dropping the block would discard; and it is the more informative
19683
+ * failure at AWS — `CreateTable` names the missing member at its exact
19684
+ * index position, where a missing block yields only
19685
+ * `ProvisionedThroughput must be specified for index: <name>`. Both
19686
+ * directions FAIL the deploy (DynamoDB requires both members on a
19687
+ * PROVISIONED index), so this is a choice of error TEXT, never of
19688
+ * outcome — what matters is that neither silently succeeds at a capacity
19689
+ * CloudFormation refuses the template for.
19690
+ *
19691
+ * **That is also why this drop needs NO `effectiveProperties`**, which
19692
+ * `.claude/rules/provider-property-fidelity.md` otherwise requires of any
19693
+ * announced narrowing: the rule exists because a SUCCEEDING call plus a
19694
+ * record describing the declared value is permanent phantom drift. Here
19695
+ * `CreateTable` FAILS, so `create()` never returns and nothing is
19696
+ * recorded — the same reasoning `tableCapacityForSend` states for the
19697
+ * table-level omit one level up.
19698
+ * - **An ABSENT member stays absent — no `5` default.** The per-index
19699
+ * reader deliberately has none ({@link readCapacityNumber}'s doc, issue
19700
+ * #1588): a defaulted per-index capacity would land in cdkd state as if
19701
+ * the template had declared it, with no later call to correct it. Only
19702
+ * the TABLE level defaults.
19703
+ * - **A block that is not a plain object is forwarded VERBATIM**, the
19704
+ * fail-OPEN direction this file takes everywhere: an unresolved intrinsic
19705
+ * or a mis-nested template value is AWS's to reject by name, and that is
19706
+ * also the pre-existing behaviour of this forwarder.
19707
+ *
19708
+ * A member name the grammar does not know is preserved untouched, for the
19709
+ * same fail-open reason — a misspelled `ReadCapacityUnit` reaches AWS and is
19710
+ * named there rather than vanishing here.
19711
+ */
19712
+ coerceIndexCapacityBlock(scope, declared, maskSecrets = (text) => text) {
19713
+ if (!isPlainCapacityBlock(declared)) return void 0;
19714
+ const block = declared;
19715
+ let rewritten;
19716
+ for (const member of PROVISIONED_CAPACITY_MEMBERS) {
19717
+ if (!(member in block)) continue;
19718
+ const coerced = readCapacityNumber(block, member);
19719
+ if (coerced === block[member]) continue;
19720
+ rewritten ??= { ...block };
19721
+ if (coerced === void 0) {
19722
+ this.warnUnusableProvisionedCapacity(scope, member, block[member], maskSecrets);
19723
+ delete rewritten[member];
19724
+ } else rewritten[member] = coerced;
19725
+ }
19726
+ return rewritten;
19727
+ }
19728
+ /**
19729
+ * Announce a `ProvisionedThroughput` member a forwarder DROPPED (issue
19730
+ * [#3147](https://github.com/go-to-k/cdkd/issues/3147); widened to the
19731
+ * per-index create forwarder by issue
19732
+ * [#3255](https://github.com/go-to-k/cdkd/issues/3255)).
19733
+ *
19734
+ * Called from all THREE forwarders that can drop one — `create()`'s
19735
+ * `CreateTable` table-level block, the BillingMode flip's `UpdateTable`
19736
+ * table-level block, and `create()`'s per-`GlobalSecondaryIndexes[]` block —
19737
+ * because the member vanishes at each independently, and a drop that is
19738
+ * never announced is the silent-narrowing class this file's other warn arms
19739
+ * exist to end. `scope` names the resource or the index, so a table with
19740
+ * several GSIs says WHICH one.
19741
+ *
19742
+ * It says NO capacity was substituted, which is the whole decision: the
19743
+ * sibling `AWS::DynamoDB::GlobalTable` forwarders announce a 5/5 FALLBACK,
19744
+ * and saying the wrong one of those two would send a user looking for a
19745
+ * table that does not exist. {@link tableCapacityForSend}'s doc carries why
19746
+ * this type differs.
19747
+ */
19748
+ warnUnusableProvisionedCapacity(scope, member, raw, maskSecrets = (text) => text) {
19749
+ const warn = (message) => this.logger.warn(maskSecrets(message));
19750
+ warn(`${scope}: ProvisionedThroughput.${member} ${JSON.stringify(maskLeafValue$1(raw, maskSecrets))} is not an Integer CloudFormation accepts (an optional sign and decimal digits only — no surrounding whitespace, hex, exponent or decimal point), so it was NOT sent and NO capacity was substituted for it. DynamoDB requires both members wherever a PROVISIONED ProvisionedThroughput block is declared — the table itself, and every GlobalSecondaryIndexes[] entry of a PROVISIONED table — and will reject the request naming this one; CloudFormation refuses the same template at properties validation. Check for an unresolved intrinsic, or spell the value as a decimal integer.`);
19408
19751
  }
19409
19752
  /**
19410
19753
  * Announce a `WarmThroughput` the send rule REFUSES (PR review of issue
@@ -23953,10 +24296,19 @@ var LogsLogGroupProvider = class {
23953
24296
  const rawPreviousRetention = previousProperties["RetentionInDays"];
23954
24297
  const renderedRetention = JSON.stringify(rawRetention);
23955
24298
  const sameRawRetention = Object.is(rawRetention, rawPreviousRetention) || renderedRetention !== void 0 && renderedRetention !== "null" && renderedRetention === JSON.stringify(rawPreviousRetention);
23956
- const retentionInDays = toCfnInteger(rawRetention);
24299
+ let retentionInDays = toCfnInteger(rawRetention);
23957
24300
  const oldRetentionInDays = toCfnInteger(rawPreviousRetention);
23958
24301
  const desiredFromAwsReadback = context?.desiredFromAwsReadback === true;
23959
- if (!sameRawRetention && !isAbsentRetention(rawRetention, desiredFromAwsReadback)) assertUsableRetention(rawRetention, retentionInDays, logicalId, resourceType);
24302
+ if (!sameRawRetention && !isAbsentRetention(rawRetention, desiredFromAwsReadback)) try {
24303
+ assertUsableRetention(rawRetention, retentionInDays, logicalId, resourceType);
24304
+ } catch (refusal) {
24305
+ if (context?.replayingState !== true || !(refusal instanceof ProvisioningError)) throw refusal;
24306
+ const legacyReading = toFiniteNumber(rawRetention);
24307
+ if (legacyReading !== void 0 && Number.isInteger(legacyReading) && legacyReading > 0) {
24308
+ retentionInDays = legacyReading;
24309
+ this.logger.warn(`${refusal.message}. This update replays a cdkd state record (rollback) written by a cdkd that read the value as ${legacyReading} and applied it, so the retention is RESTORED as ${legacyReading} days rather than refused. Fix the template spelling and re-deploy.`);
24310
+ } else this.logger.warn(`${refusal.message}. This update replays a cdkd state record (rollback), so the value is read as the "no retention" the record meant rather than refused: the log group's retention policy is REMOVED, which is what the cdkd that wrote the record left in place. Re-apply it by hand (aws logs put-retention-policy) or fix the template and re-deploy.`);
24311
+ }
23960
24312
  const newKmsKeyId = properties["KmsKeyId"];
23961
24313
  if (newKmsKeyId !== previousProperties["KmsKeyId"]) {
23962
24314
  if (newKmsKeyId) await this.logsClient.send(new AssociateKmsKeyCommand({
@@ -25630,7 +25982,13 @@ var SSMParameterProvider = class {
25630
25982
  await this.ssmClient.send(new DeleteParameterCommand({ Name: name }));
25631
25983
  debug(`Cleaned up partially-created SSM parameter ${logicalId} (${name}) after wiring failure`);
25632
25984
  } catch (cleanupError) {
25633
- warn(`Failed to clean up partially-created SSM parameter ${logicalId} (${name}): ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}. Manual deletion may be required before the next deploy: aws ssm delete-parameter --name '${name}'`);
25985
+ const deleteCommand = renderDisableCommand({
25986
+ before: "aws ssm delete-parameter --name",
25987
+ identifier: name,
25988
+ maskSecrets: mask
25989
+ });
25990
+ const manualStep = deleteCommand ? `Manual deletion may be required before the next deploy: ${deleteCommand}` : "Manual deletion may be required before the next deploy, via the console: the parameter name cannot be reproduced safely on a command line, and a command naming the sanitized form would delete a DIFFERENT parameter.";
25991
+ warn(`Failed to clean up partially-created SSM parameter ${logicalId} (${name}): ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}. ${manualStep}`);
25634
25992
  }
25635
25993
  throw innerError;
25636
25994
  }
@@ -25858,7 +26216,13 @@ var SSMParameterProvider = class {
25858
26216
  const shape = isArn ? "an ARN" : "a version / label selector";
25859
26217
  const remedy = input.knownPhysicalId === explicit ? `pass the parameter NAME instead: --resource ${input.logicalId}=<parameterName>` : `set Properties.Name to the parameter NAME rather than ${shape}`;
25860
26218
  const whyNotDerived = isArn ? " cdkd deliberately does NOT derive the name from the ARN: the leading \"/\" is not recoverable (arn:...:parameter/foo is the ARN of BOTH the simple name \"foo\" and the hierarchical name \"/foo\"), and a parameter shared from another account has no name form at all — so a derived name could silently address a DIFFERENT parameter." : "";
25861
- throw new ProvisioningError(`Cannot adopt SSM parameter ${input.logicalId} from ${shape} ('${explicit}'): cdkd records a parameter's NAME as its physical id, because SSM's write APIs accept only the name (PutParameter and DeleteParameter both reject an ARN, and a name cannot contain ':'), so the next cdkd deploy and cdkd destroy would fail with a ValidationException. ${remedy}.${whyNotDerived} Read the name AWS holds with: aws ssm get-parameter --name '${explicit}' --query Parameter.Name --output text`, input.resourceType, input.logicalId);
26219
+ const readCommand = renderDisableCommand({
26220
+ before: "aws ssm get-parameter --name",
26221
+ identifier: explicit,
26222
+ after: "--query Parameter.Name --output text"
26223
+ });
26224
+ const howToRead = readCommand ? ` Read the name AWS holds with: ${readCommand}` : " Read the name AWS holds via the console: the value cdkd was given cannot be reproduced safely on a command line, so any command shown here would read a different parameter.";
26225
+ throw new ProvisioningError(`Cannot adopt SSM parameter ${input.logicalId} from ${shape} ('${explicit}'): cdkd records a parameter's NAME as its physical id, because SSM's write APIs accept only the name (PutParameter and DeleteParameter both reject an ARN, and a name cannot contain ':'), so the next cdkd deploy and cdkd destroy would fail with a ValidationException. ${remedy}.${whyNotDerived}${howToRead}`, input.resourceType, input.logicalId);
25862
26226
  }
25863
26227
  /**
25864
26228
  * Adopt an existing SSM parameter into cdkd state.
@@ -65936,7 +66300,7 @@ function indexNestedChildTemplates(template, templatePath) {
65936
66300
  if (resource?.Type !== "AWS::CloudFormation::Stack") continue;
65937
66301
  const assetPath = resource.Metadata?.["aws:asset:path"];
65938
66302
  if (typeof assetPath !== "string" || assetPath.length === 0) continue;
65939
- if (isAbsoluteCrossPlatform(assetPath)) throw new Error(`Nested stack '${logicalId}' has Metadata['aws:asset:path']='${assetPath}' which is absolute. CDK emits relative asset paths for nested templates; an absolute path indicates the synth output was hand-modified or generated by a non-CDK toolchain. Refusing to load.`);
66303
+ if (isAbsoluteCrossPlatform(assetPath)) throw new Error(`Nested stack '${displaySafe(logicalId)}' has Metadata['aws:asset:path']='${displaySafe(assetPath)}' which is absolute. CDK emits relative asset paths for nested templates; an absolute path indicates the synth output was hand-modified or generated by a non-CDK toolchain. Refusing to load.`);
65940
66304
  result[logicalId] = path.join(dir, assetPath);
65941
66305
  }
65942
66306
  return result;
@@ -66236,7 +66600,7 @@ async function resolveChildStackParameters(parentStackRow, parentTemplate, paren
66236
66600
  * fail at deploy time.
66237
66601
  */
66238
66602
  async function buildDiffTree(args) {
66239
- const { stackName, displayName, region, template, nestedTemplates, recursive, stateBackend, diffCalculator, parentHasSecretReference, parameters, canonicalizeProperties, assetRedirect, cfnFallback, previewOrphanAdoption } = args;
66603
+ const { stackName, displayName, region, template, nestedTemplates, recursive, stateBackend, diffCalculator, parentHasSecretReference, parameters, canonicalizeProperties, assetRedirect, cfnFallback, previewOrphanAdoption, ancestorTemplatePaths } = args;
66240
66604
  const state = await loadStateOrEmpty(stackName, region, stateBackend);
66241
66605
  const secretBearingAbove = parentHasSecretReference === true || templateHasSecretDynamicReference(template);
66242
66606
  const { changes, outputChanges, adoptedOrphans, adoptedRecords, blocking } = await computeStackDiff(state, template, region, stackName, stateBackend, diffCalculator, {
@@ -66272,6 +66636,9 @@ async function buildDiffTree(args) {
66272
66636
  const childTemplatePath = nestedTemplates[logicalId];
66273
66637
  if (!childTemplatePath) throw new Error(`Nested template file not found for ${NESTED_STACK_RESOURCE_TYPE} '${logicalId}' under stack '${stackName}'. Verify the synth output emits Metadata['aws:asset:path'] on this resource (CDK 2.x cdk.NestedStack does so by default), then re-run synth.`);
66274
66638
  const childStackName = `${stackName}~${logicalId}`;
66639
+ const resolvedChildPath = path.resolve(childTemplatePath);
66640
+ if (ancestorTemplatePaths?.has(resolvedChildPath) === true) throw new Error(`Nested stack '${displaySafe(logicalId)}' under stack '${displaySafe(stackName)}' resolves to nested template '${displaySafe(resolvedChildPath)}', which is already being diffed higher up the same nesting chain. Its Metadata['aws:asset:path'] closes a cycle; CDK emits an acyclic nested template tree, so this indicates the synth output was hand-modified or generated by a non-CDK toolchain. Refusing to diff.`);
66641
+ const childAncestorTemplatePaths = new Set(ancestorTemplatePaths ?? []).add(resolvedChildPath);
66275
66642
  const childTemplate = readNestedTemplate(childTemplatePath);
66276
66643
  if (assetRedirect) rewriteTemplateAssetReferences(childTemplate, assetRedirect);
66277
66644
  const grandchildTemplates = indexNestedChildTemplates(childTemplate, childTemplatePath);
@@ -66290,6 +66657,7 @@ async function buildDiffTree(args) {
66290
66657
  ...assetRedirect && { assetRedirect },
66291
66658
  ...cfnFallback !== void 0 && { cfnFallback },
66292
66659
  ...previewOrphanAdoption && { previewOrphanAdoption },
66660
+ ancestorTemplatePaths: childAncestorTemplatePaths,
66293
66661
  parentHasSecretReference: secretBearingAbove
66294
66662
  }));
66295
66663
  }
@@ -74531,6 +74899,39 @@ function createStateMigrateCommand() {
74531
74899
  //#endregion
74532
74900
  //#region src/cli/commands/state-list-tree.ts
74533
74901
  /**
74902
+ * How deep a node may sit below its root, counting the root as 0.
74903
+ *
74904
+ * A node that would sit deeper is made a root instead (see
74905
+ * {@link buildStackTree}). The value is far beyond any nesting an app is
74906
+ * written with: CloudFormation stops at five levels of nested stacks, so an
74907
+ * app that also has to deploy through CloudFormation cannot exceed that.
74908
+ * cdkd deploys nested stacks through its own engine and imposes no limit of
74909
+ * its own, so a deliberately deeper app CAN produce a tree this reshapes —
74910
+ * which is the point. The view stays whole either way.
74911
+ *
74912
+ * It exists because every step AFTER the tree is built recurses once per
74913
+ * level: this module's own child sort, the box-drawing renderer and
74914
+ * {@link stackTreeToJson}, plus the `JSON.stringify` that serializes that
74915
+ * shape for `--tree --json`. Capping the tree bounds all four at once, which
74916
+ * an iterative rewrite of any of them could not: `JSON.stringify` recurses
74917
+ * into whatever nesting it is handed, and the text view indents four
74918
+ * characters per level, so a chain deep enough would need more indentation
74919
+ * than a string can hold.
74920
+ *
74921
+ * It bounds the DEPTH, not the bucket, and what it does to the failure at the
74922
+ * far end is change its KIND. Before the cap, one deep chain exhausted the
74923
+ * CALL STACK, at a record count nothing fixes — V8's budget moves with frame
74924
+ * size and with what else is on the stack. After it, every recursion is at
74925
+ * most a hundred frames and the remaining ceiling is V8's maximum STRING
74926
+ * length: a capped tree still costs a roughly constant number of characters
74927
+ * per record (the cap's own pretty-print indent dominates in `--tree --json`),
74928
+ * so `JSON.stringify` refuses a large enough bucket deterministically instead
74929
+ * of at a moving depth. Both are far past `renderTreeMode`'s unbounded
74930
+ * one-read-per-reference fan-out, which gives out first. Read the cap as what
74931
+ * stops a SINGLE chain taking the view down, not as a total bound.
74932
+ */
74933
+ const MAX_STACK_TREE_DEPTH = 100;
74934
+ /**
74534
74935
  * Build a parent → child tree from the flat list of state records.
74535
74936
  *
74536
74937
  * Children are linked to their parent by `(parentStack, parentRegion)`
@@ -74546,6 +74947,17 @@ function createStateMigrateCommand() {
74546
74947
  * from both the text and the JSON view. A node hanging off a loop still sits
74547
74948
  * under its parent, which is now visible (issue #3069).
74548
74949
  *
74950
+ * A node that would sit deeper than {@link MAX_STACK_TREE_DEPTH} levels below
74951
+ * its root is placed at the root too, so the chain resumes there as its own
74952
+ * tree. Every record still appears exactly once, and a tree within the cap is
74953
+ * built as before. Without it a long enough chain took the whole view down with
74954
+ * a `RangeError` instead of costing only its own rows, since the recursive
74955
+ * steps AFTER this function are not covered by the per-stack guard around the
74956
+ * state read. Reaching the depths that failed takes thousands of records — in
74957
+ * practice a hand-edited bucket, or records planted by someone holding
74958
+ * `s3:PutObject` on it — but nothing in cdkd bounds the depth, so the cap does
74959
+ * (issue #3155).
74960
+ *
74549
74961
  * The roots and every child list are sorted alphabetically by `stackName`,
74550
74962
  * then by `region` (legacy `undefined` last), so output is stable across
74551
74963
  * runs.
@@ -74572,10 +74984,26 @@ function buildStackTree(entries) {
74572
74984
  if (current !== void 0 && onPath.has(current)) for (const member of path.slice(onPath.get(current))) loopMembers.add(member);
74573
74985
  for (const node of path) settled.add(node);
74574
74986
  }
74987
+ const depthOf = /* @__PURE__ */ new Map();
74988
+ for (const member of loopMembers) depthOf.set(member, 0);
74989
+ for (const start of byKey.values()) {
74990
+ if (depthOf.has(start)) continue;
74991
+ const path = [];
74992
+ let current = start;
74993
+ while (current !== void 0 && !depthOf.has(current)) {
74994
+ path.push(current);
74995
+ current = parentOf(current);
74996
+ }
74997
+ let depth = current === void 0 ? -1 : depthOf.get(current);
74998
+ for (let i = path.length - 1; i >= 0; i--) {
74999
+ depth = depth + 1 > 100 ? 0 : depth + 1;
75000
+ depthOf.set(path[i], depth);
75001
+ }
75002
+ }
74575
75003
  const roots = [];
74576
75004
  for (const node of byKey.values()) {
74577
75005
  const parent = parentOf(node);
74578
- if (parent && !loopMembers.has(node)) {
75006
+ if (parent && depthOf.get(node) > 0) {
74579
75007
  parent.children.push(node);
74580
75008
  continue;
74581
75009
  }
@@ -78609,6 +79037,12 @@ async function stateListCommand(options) {
78609
79037
  *
78610
79038
  * `tree --json` emits the nested {@link import('./state-list-tree.js').StackTreeJson}
78611
79039
  * shape; plain `tree` renders `tree(1)`-style box-drawing.
79040
+ *
79041
+ * The `JSON.stringify` below recurses once per level of the shape it is handed,
79042
+ * and so does the box-drawing renderer. Neither needs a depth guard here:
79043
+ * `buildStackTree` caps how deep a node may sit
79044
+ * ({@link import('./state-list-tree.js').MAX_STACK_TREE_DEPTH}), which bounds
79045
+ * both (issue #3155).
78612
79046
  */
78613
79047
  async function renderTreeMode(refs, stateBackend, asJson) {
78614
79048
  const entries = await Promise.all(refs.map(async (ref) => {
@@ -94054,4 +94488,4 @@ function buildProgram() {
94054
94488
 
94055
94489
  //#endregion
94056
94490
  export { buildProgram };
94057
- //# sourceMappingURL=program-DD0X9qLM.js.map
94491
+ //# sourceMappingURL=program-U03H43s7.js.map