@go-to-k/cdkd 0.278.35 → 0.278.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as describeTypeWithThrottleRetry, $t as AwsClients, A as red, At as Synthesizer, B as isTerminationProtectionPropagationError, Bt as resolveUseCdkBootstrapAssets, C as isStatefulRecreateTargetSync, Ct as buildDockerImage, D as cyan, Dt as runDockerStreaming, E as bold, Et as runDockerForeground, F as ProviderRegistry, Ft as resolveAutoAssetStorage, G as normalizeAwsTagsToCfn, Gt as MIGRATE_TMP_PREFIX, H as cfnRefValueFromPhysicalId, Ht as warnDeprecatedNoPrefixCliFlag, I as findActionableSilentDrops, It as resolveCaptureObservedState, J as readConfigString, Jt as expectedOwnerParam, K as resolveExplicitPhysicalId, Kt as findLargeInlineResources, L as CloudControlProvider, Lt as resolveSkipPrefix, M as IAMRoleProvider, Mt as getDefaultStateBucketName, N as collectInlinePolicyNamesManagedBySiblings, Nt as getLegacyStateBucketName, O as gray, Ot as AssetManifestLoader, P as clearOnUpdateRemoval, Pt as resolveApp, Q as DiffCalculator, Qt as resolveBucketRegion, R as slowCcOperationTimeoutMs, Rt as resolveStateBucketWithDefault, S as MULTI_REGION_RECREATE_BLOCKED_TYPES, St as validateContainerRepoName, T as formatResourceLine, Tn as withErrorHandling, Tt as getDockerCmd, U as refStateLookupFromResource, Ut as CFN_TEMPLATE_BODY_LIMIT, V as IntrinsicFunctionResolver, Vt as stateBucketExistenceConfirmed, W as WAFv2WebACLProvider, Wt as CFN_TEMPLATE_URL_LIMIT, X as requireConfigString, Xt as processStackMessages, Y as requireConfigArray, Yt as AssemblyReader, Z as applyRoleArnIfSet, _ as createPreDeleteFinalSnapshot, _n as ResourceUpdateNotSupportedError, _t as BOOTSTRAP_MARKER_PREFIX, a as DeploymentEventsStore, at as LockManager, b as unsupportedFinalSnapshotError, bt as parseBootstrapMarker, c as replayFailedOperations, cn as LocalInvokeBuildError$1, ct as shouldRetainResource, d as IMPLICIT_DELETE_DEPENDENCIES, dt as WorkGraph, en as getAwsClients, et as withRetry, f as computeImplicitDeleteEdges, fn as MissingCdkCliError, ft as buildAssetRedirectMap, g as ccRoutedFinalSnapshotError, gn as ResourceTimeoutError, gt as AssetModeResolver, h as buildFinalSnapshotIdentifier, hn as ProvisioningError, ht as rewriteTemplateAssetReferences, i as DeploymentEventsReader, in as CdkdError, it as TemplateParser, j as yellow, jt as synthesisStatusMessage, k as green, kt as getDockerImageBySourceHash, l as replayRollback, ln as LocalMigrateError, lt as AssetPublisher, m as PRE_DELETE_SNAPSHOT_TYPES, mn as PartialFailureError, mt as loadPublishableAssetManifest, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as setAwsClients, nt as isThrottlingError, o as planFailedOps, ot as S3StateBackend, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as NestedStackChildDirectDestroyError, pt as createAssetRedirectResolver, q as assertRegionMatch, qt as uploadCfnTemplate, r as DeployEngine, rt as DagBuilder, s as planRollback, sn as DeployCancelledError, st as rebuildClientForBucketRegion, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resetAwsClients, tt as isRetryableTransientError, u as withResourceDeadline, un as LocalStartServiceError, ut as stringifyValue, v as isFinalSnapshotError, vn as StackHasActiveImportsError, vt as ensureAssetStorage, w as renderStatefulReason, wn as normalizeAwsError, wt as formatDockerLoginError, x as extractDeploymentEventError, xt as validateAssetBucketName, y as refusesFinalSnapshot, yn as StackTerminationProtectionError, yt as getBootstrapMarkerKey, z as disableInstanceApiTermination, zt as resolveStateBucketWithDefaultAndSource } from "./deploy-engine-CWZlo2Il.js";
2
+ import { $ as describeTypeWithThrottleRetry, $t as AwsClients, A as red, At as Synthesizer, B as isTerminationProtectionPropagationError, Bt as resolveUseCdkBootstrapAssets, C as isStatefulRecreateTargetSync, Ct as buildDockerImage, D as cyan, Dt as runDockerStreaming, E as bold, Et as runDockerForeground, F as ProviderRegistry, Ft as resolveAutoAssetStorage, G as normalizeAwsTagsToCfn, Gt as MIGRATE_TMP_PREFIX, H as cfnRefValueFromPhysicalId, Ht as warnDeprecatedNoPrefixCliFlag, I as findActionableSilentDrops, It as resolveCaptureObservedState, J as readConfigString, Jt as expectedOwnerParam, K as resolveExplicitPhysicalId, Kt as findLargeInlineResources, L as CloudControlProvider, Lt as resolveSkipPrefix, M as IAMRoleProvider, Mt as getDefaultStateBucketName, N as collectInlinePolicyNamesManagedBySiblings, Nt as getLegacyStateBucketName, O as gray, Ot as AssetManifestLoader, P as clearOnUpdateRemoval, Pt as resolveApp, Q as DiffCalculator, Qt as resolveBucketRegion, R as slowCcOperationTimeoutMs, Rt as resolveStateBucketWithDefault, S as MULTI_REGION_RECREATE_BLOCKED_TYPES, St as validateContainerRepoName, T as formatResourceLine, Tn as withErrorHandling, Tt as getDockerCmd, U as refStateLookupFromResource, Ut as CFN_TEMPLATE_BODY_LIMIT, V as IntrinsicFunctionResolver, Vt as stateBucketExistenceConfirmed, W as WAFv2WebACLProvider, Wt as CFN_TEMPLATE_URL_LIMIT, X as requireConfigString, Xt as processStackMessages, Y as requireConfigArray, Yt as AssemblyReader, Z as applyRoleArnIfSet, _ as createPreDeleteFinalSnapshot, _n as ResourceUpdateNotSupportedError, _t as BOOTSTRAP_MARKER_PREFIX, a as DeploymentEventsStore, at as LockManager, b as unsupportedFinalSnapshotError, bt as parseBootstrapMarker, c as replayFailedOperations, cn as LocalInvokeBuildError$1, ct as shouldRetainResource, d as IMPLICIT_DELETE_DEPENDENCIES, dt as WorkGraph, en as getAwsClients, et as withRetry, f as computeImplicitDeleteEdges, fn as MissingCdkCliError, ft as buildAssetRedirectMap, g as ccRoutedFinalSnapshotError, gn as ResourceTimeoutError, gt as AssetModeResolver, h as buildFinalSnapshotIdentifier, hn as ProvisioningError, ht as rewriteTemplateAssetReferences, i as DeploymentEventsReader, in as CdkdError, it as TemplateParser, j as yellow, jt as synthesisStatusMessage, k as green, kt as getDockerImageBySourceHash, l as replayRollback, ln as LocalMigrateError, lt as AssetPublisher, m as PRE_DELETE_SNAPSHOT_TYPES, mn as PartialFailureError, mt as loadPublishableAssetManifest, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as setAwsClients, nt as isThrottlingError, o as planFailedOps, ot as S3StateBackend, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as NestedStackChildDirectDestroyError, pt as createAssetRedirectResolver, q as assertRegionMatch, qt as uploadCfnTemplate, r as DeployEngine, rt as DagBuilder, s as planRollback, sn as DeployCancelledError, st as rebuildClientForBucketRegion, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resetAwsClients, tt as isRetryableTransientError, u as withResourceDeadline, un as LocalStartServiceError, ut as stringifyValue, v as isFinalSnapshotError, vn as StackHasActiveImportsError, vt as ensureAssetStorage, w as renderStatefulReason, wn as normalizeAwsError, wt as formatDockerLoginError, x as extractDeploymentEventError, xt as validateAssetBucketName, y as refusesFinalSnapshot, yn as StackTerminationProtectionError, yt as getBootstrapMarkerKey, z as disableInstanceApiTermination, zt as resolveStateBucketWithDefaultAndSource } from "./deploy-engine-bEhHOGVR.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as withSkipPrefix, i as runStackBuffered, n as getLogger, o as PATTERN_B_NAME_OPTIONS, p as withStackName, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-BYMEE-BS.js";
4
- import { t as ASGProvider } from "./asg-provider-8XxBHtzK.js";
4
+ import { t as ASGProvider } from "./asg-provider-BTeSlton.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";
@@ -66,7 +66,8 @@ import { CodeCommitClient, CreateCommitCommand, CreateRepositoryCommand as Creat
66
66
  import AdmZip from "adm-zip";
67
67
  import { CreateLifecyclePolicyCommand, DLMClient, DeleteLifecyclePolicyCommand as DeleteLifecyclePolicyCommand$1, GetLifecyclePolicyCommand as GetLifecyclePolicyCommand$1, ResourceNotFoundException as ResourceNotFoundException$14, TagResourceCommand as TagResourceCommand$21, UntagResourceCommand as UntagResourceCommand$21, UpdateLifecyclePolicyCommand } from "@aws-sdk/client-dlm";
68
68
  import { CreateVectorBucketCommand, DeleteIndexCommand, DeleteVectorBucketCommand, GetVectorBucketCommand, ListIndexesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$17, S3VectorsClient, TagResourceCommand as TagResourceCommand$22, UntagResourceCommand as UntagResourceCommand$22 } from "@aws-sdk/client-s3vectors";
69
- import { CreateNamespaceCommand, CreateTableBucketCommand, CreateTableCommand as CreateTableCommand$2, DeleteNamespaceCommand as DeleteNamespaceCommand$1, DeleteTableBucketCommand, DeleteTableCommand as DeleteTableCommand$2, GetTableBucketCommand, GetTableCommand as GetTableCommand$1, ListNamespacesCommand as ListNamespacesCommand$1, ListTableBucketsCommand, ListTablesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$18, NotFoundException as NotFoundException$6, S3TablesClient, TagResourceCommand as TagResourceCommand$23, UntagResourceCommand as UntagResourceCommand$23 } from "@aws-sdk/client-s3tables";
69
+ import { ListTagsForResourceCommand as ListTagsForResourceCommand$18, S3ControlClient, TagResourceCommand as TagResourceCommand$23, UntagResourceCommand as UntagResourceCommand$23 } from "@aws-sdk/client-s3-control";
70
+ import { CreateNamespaceCommand, CreateTableBucketCommand, CreateTableCommand as CreateTableCommand$2, DeleteNamespaceCommand as DeleteNamespaceCommand$1, DeleteTableBucketCommand, DeleteTableCommand as DeleteTableCommand$2, GetTableBucketCommand, GetTableCommand as GetTableCommand$1, ListNamespacesCommand as ListNamespacesCommand$1, ListTableBucketsCommand, ListTablesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$19, NotFoundException as NotFoundException$6, S3TablesClient, TagResourceCommand as TagResourceCommand$24, UntagResourceCommand as UntagResourceCommand$24 } from "@aws-sdk/client-s3tables";
70
71
  import { Document, Pair, Scalar, YAMLMap, YAMLSeq, parse as parse$1, stringify } from "yaml";
71
72
  import { createLocalStartAgentCoreCommand, createLocalStartCloudFrontCommand, createLocalStateProvider, getEmbedConfig, isCfnFlagPresent, listTargets, rejectExplicitCfnStackWithMultipleStacks, resolveCfnFallbackRegion, setEmbedConfig, substituteAgainstState, substituteAgainstStateAsync, substituteEnvVarsFromState, substituteEnvVarsFromStateAsync } from "cdk-local";
72
73
  import { A2A_CONTAINER_PORT, A2A_PATH, AGENTCORE_A2A_PROTOCOL, AGENTCORE_AGUI_PROTOCOL, AGENTCORE_MCP_PROTOCOL, ConnectionRegistry, EcsTaskResolutionError, HOST_GATEWAY_MIN_VERSION, LocalInvokeBuildError, MCP_CONTAINER_PORT, MCP_PATH, a2aInvokeOnce, addAlbSpecificOptions, addCommonEcsServiceOptions, addStartServiceSpecificOptions, albStrategy, architectureToPlatform, attachAuthorizers, attachStageContext, availableApiIdentifiers, bufferToBody, buildAgentCoreCodeImage, buildCognitoJwksUrl, buildConnectEvent, buildContainerImage, buildCorsConfigByApiId, buildCorsConfigFromCloudFrontChain, buildDisconnectEvent, buildJwksUrlFromIssuer, buildMessageEvent, buildMgmtEndpointEnvUrl, buildStageMap, classifySourceChange, createAuthorizerCache, createFileWatcher, createFileWatcher as createFileWatcher$1, createJwksCache, createWatchPredicates, defaultCredentialsLoader, derivePseudoParametersFromRegion, discoverRoutes, discoverWebSocketApis, downloadAndExtractS3Bundle, filterRoutesByApiIdentifier, groupRoutesByServer, handleConnectionsRequest, invokeAgentCore, invokeAgentCoreWs, materializeLayerFromArn, mcpInvokeOnce, parseConnectionsPath, parseSelectionExpressionPath, pickAgentCoreCandidateStack, pickAgentCoreCandidateStack as pickAgentCoreCandidateStack$1, probeHostGatewaySupport, readMtlsMaterialsFromDisk, resolveAgentCoreTarget, resolveEnvVars, resolveHostGatewayExtraHosts, resolveRuntimeCodeMountPath, resolveRuntimeFileExtension, resolveRuntimeImage, resolveSingleTarget, resolveWatchConfig, runEcsServiceEmulator, signAgentCoreInvocation, startApiServer, substituteImagePlaceholders, tryResolveImageFnJoin, verifyJwtViaDiscovery, waitForAgentCorePing } from "cdk-local/internal";
@@ -12314,7 +12315,8 @@ var DynamoDBGlobalTableProvider = class {
12314
12315
  AttributeDefinitions: attributeDefinitions,
12315
12316
  BillingMode: billingMode
12316
12317
  };
12317
- if (billingMode === "PROVISIONED") createParams.ProvisionedThroughput = derivePerCallProvisionedThroughput(properties, currentRegion);
12318
+ const diagnostics = [];
12319
+ if (billingMode === "PROVISIONED") createParams.ProvisionedThroughput = derivePerCallProvisionedThroughput(properties, currentRegion, "min", diagnostics);
12318
12320
  const streamSpecInput = properties["StreamSpecification"];
12319
12321
  const needsStream = replicas.some((r) => r["Region"] !== currentRegion) || replicas.length > 1;
12320
12322
  if (streamSpecInput != null) {
@@ -12335,7 +12337,6 @@ var DynamoDBGlobalTableProvider = class {
12335
12337
  StreamViewType: "NEW_AND_OLD_IMAGES"
12336
12338
  };
12337
12339
  }
12338
- const diagnostics = [];
12339
12340
  let sdkIndexes;
12340
12341
  try {
12341
12342
  sdkIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, billingMode, "min", diagnostics);
@@ -12551,7 +12552,10 @@ var DynamoDBGlobalTableProvider = class {
12551
12552
  const onDemandCeilingLive = oldBilling === "PAY_PER_REQUEST" && newBilling === "PAY_PER_REQUEST";
12552
12553
  const diagnostics = [];
12553
12554
  const desiredCeilings = collectTableOnDemandCeilings(properties, currentRegion, diagnostics);
12554
- const desiredSdkIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling, "min", diagnostics);
12555
+ const flippingToProvisioned = oldBilling !== newBilling && newBilling === "PROVISIONED";
12556
+ if (flippingToProvisioned) derivePerCallProvisionedThroughput(properties, currentRegion, "seed", diagnostics);
12557
+ const desiredSdkIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling, "min", flippingToProvisioned ? void 0 : diagnostics);
12558
+ if (flippingToProvisioned) toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling, "seed", diagnostics);
12555
12559
  for (const replica of properties["Replicas"] ?? []) {
12556
12560
  const replicaRegion = asRecord$1(replica)?.["Region"];
12557
12561
  if (typeof replicaRegion !== "string" || replicaRegion === currentRegion) continue;
@@ -13231,9 +13235,11 @@ var DynamoDBGlobalTableProvider = class {
13231
13235
  const cause = regionErr instanceof Error ? regionErr : void 0;
13232
13236
  throw new ProvisioningError(`Could not resolve client region for DynamoDB GlobalTable ${logicalId} delete — would risk dropping the local replica`, resourceType, logicalId, physicalId, cause);
13233
13237
  }
13238
+ let preDeleteIndexes;
13234
13239
  try {
13235
13240
  const describe = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }));
13236
13241
  const replicas = describe.Table?.Replicas ?? [];
13242
+ preDeleteIndexes = describe.Table?.GlobalSecondaryIndexes;
13237
13243
  const tableIndexNames = (describe.Table?.GlobalSecondaryIndexes ?? []).map((gsi) => gsi.IndexName).filter((name) => typeof name === "string");
13238
13244
  for (const replica of replicas) {
13239
13245
  const region = replica.RegionName;
@@ -13264,6 +13270,10 @@ var DynamoDBGlobalTableProvider = class {
13264
13270
  }
13265
13271
  }
13266
13272
  try {
13273
+ if (hasTransitionalIndex(preDeleteIndexes)) {
13274
+ this.logger.debug(`Waiting for indexes on ${physicalId} to settle before DeleteTable (issue #1521)`);
13275
+ await this.waitForIndexesActive(physicalId, logicalId);
13276
+ }
13267
13277
  await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
13268
13278
  await this.waitForTableGone(physicalId, logicalId);
13269
13279
  this.logger.debug(`Successfully deleted DynamoDB GlobalTable ${logicalId}`);
@@ -13678,34 +13688,78 @@ var DynamoDBGlobalTableProvider = class {
13678
13688
  * `waitForTableActive`, this tolerates any non-terminal status — the
13679
13689
  * table may already be ACTIVE on the no-op path (already-disabled
13680
13690
  * protection) or transition through UPDATING.
13691
+ *
13692
+ * **A table is ACTIVE while one of its INDEXES is still transitioning**, and
13693
+ * the next `UpdateTable` in the serialized sequence is then rejected with
13694
+ * `Attempt to change a resource which is still in use: Index is being
13695
+ * updated` (issue #1521). That is not hypothetical: it broke the
13696
+ * `PAY_PER_REQUEST -> PROVISIONED` flip that also drops a GSI — AWS applies
13697
+ * the flip to every index, and cdkd issued the index DELETE while the index
13698
+ * was still absorbing it. Reproduced on pristine `main`, three runs out of
13699
+ * four on 2026-08-10, and the failed deploy's own `cdkd destroy` then
13700
+ * ORPHANED the table for the same reason.
13701
+ *
13702
+ * So the wait has TWO conditions read off the SAME response (an extra
13703
+ * `DescribeTable` per wait would double this path's API traffic for
13704
+ * something the response already carries) — but they keep DIFFERENT failure
13705
+ * semantics, and conflating them is a real regression rather than a
13706
+ * simplification. A table that never reaches ACTIVE is a hard error: nothing
13707
+ * can proceed. A still-transitioning INDEX is best-effort: `IndexStatus`
13708
+ * stays `CREATING` for the whole BACKFILL of a newly added GSI, which on a
13709
+ * populated table routinely outlives any cap — so throwing there would fail
13710
+ * a deploy whose resources are all correct, and would do it on an unrelated
13711
+ * tag-only edit that merely ran while an earlier index was still building.
13712
+ * Past the cap the index half warns and proceeds, leaving AWS's own
13713
+ * rejection as the backstop, which is the behavior it had before #1521 for
13714
+ * every case except the one the wait exists to fix.
13681
13715
  */
13682
13716
  async waitForTableActiveAfterUpdate(tableName, logicalId, maxAttempts = 600) {
13717
+ let tableReachedActive = false;
13683
13718
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
13684
- if ((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.TableStatus === "ACTIVE") return;
13719
+ const response = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }));
13720
+ if (response.Table?.TableStatus === "ACTIVE") {
13721
+ tableReachedActive = true;
13722
+ if (!hasTransitionalIndex(response.Table?.GlobalSecondaryIndexes)) return;
13723
+ }
13685
13724
  await new Promise((resolve) => setTimeout(resolve, 1e3));
13686
13725
  }
13726
+ if (tableReachedActive) {
13727
+ this.logger.warn(`Indexes on ${tableName} (${logicalId}) were still transitioning after ${maxAttempts}s — a large index backfill can outlive this wait. Proceeding; AWS rejects the next call if it is still too early.`);
13728
+ return;
13729
+ }
13687
13730
  throw new ProvisioningError(`Table ${tableName} did not reach ACTIVE within ${maxAttempts}s after UpdateTable`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
13688
13731
  }
13689
13732
  /**
13690
13733
  * Wait until every GSI reports `IndexStatus: ACTIVE` (Issue #1419).
13691
13734
  *
13692
- * `waitForTableActiveAfterUpdate` only checks `TableStatus`, and a GSI
13693
- * added by `UpdateTable` leaves the TABLE ACTIVE while the index itself
13694
- * is still `CREATING` — application-autoscaling rejects a target whose
13695
- * resource is not ready, so registering right after the GSI-add loop
13735
+ * A GSI added by `UpdateTable` leaves the TABLE ACTIVE while the index
13736
+ * itself is still `CREATING` — application-autoscaling rejects a target
13737
+ * whose resource is not ready, so registering right after the GSI-add loop
13696
13738
  * would skip the very index the deploy just introduced.
13697
13739
  *
13740
+ * Since issue #1521 the same distinction is enforced INSIDE
13741
+ * {@link waitForTableActiveAfterUpdate} (which now reads both conditions off
13742
+ * its own response), so this helper is no longer the only place that knows
13743
+ * it. It remains the explicit wait for the two callers that have no
13744
+ * table-status wait to piggyback on: the autoscaling registration above and
13745
+ * the pre-`DeleteTable` gate, which AWS rejects while an index is
13746
+ * transitioning. An index that is `DELETING` still reports, so the shared
13747
+ * {@link hasTransitionalIndex} predicate covers `CREATING` / `UPDATING` /
13748
+ * `DELETING` alike.
13749
+ *
13698
13750
  * **Best-effort by design**: on timeout it warns and returns rather than
13699
13751
  * throwing. A missed registration self-heals on the next deploy (the
13700
13752
  * reconcile re-asserts anything not already present), whereas throwing
13701
13753
  * here would fail a deploy whose actual resources are all correct. Index
13702
13754
  * backfill on a large table can take a long time, hence the 15-minute cap.
13755
+ * The caller that follows it with a mutating call keeps AWS's own error as
13756
+ * the backstop for the case where even that cap is not enough.
13703
13757
  */
13704
13758
  async waitForIndexesActive(tableName, logicalId) {
13705
13759
  const maxAttempts = 900;
13706
13760
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
13707
13761
  try {
13708
- if (((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.GlobalSecondaryIndexes ?? []).every((gsi) => gsi.IndexStatus === "ACTIVE")) return;
13762
+ if (!hasTransitionalIndex((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.GlobalSecondaryIndexes ?? [])) return;
13709
13763
  } catch (err) {
13710
13764
  this.logger.debug(`DescribeTable while waiting for indexes on ${tableName}: ${err instanceof Error ? err.message : String(err)}`);
13711
13765
  return;
@@ -13809,10 +13863,17 @@ function extractLocalDeletionProtection(props, region) {
13809
13863
  * literal `ReadCapacityUnits`, which is taken before its auto-scaling
13810
13864
  * members; 5 if absent.
13811
13865
  */
13812
- function derivePerCallProvisionedThroughput(properties, region, source = "min") {
13866
+ function derivePerCallProvisionedThroughput(properties, region, source = "min", diagnostics) {
13867
+ const rawReplicas = properties["Replicas"];
13868
+ const rawRead = (Array.isArray(rawReplicas) ? rawReplicas : []).find((r) => asRecord$1(r)?.["Region"] === region)?.["ReadProvisionedThroughputSettings"];
13869
+ const rawWrite = properties["WriteProvisionedThroughputSettings"];
13870
+ const read = deriveReadCapacityUnits(asRecord$1(rawRead), source);
13871
+ const write = deriveWriteCapacityUnits(asRecord$1(rawWrite), source);
13872
+ if (read === void 0) reportUnresolvedProvisionedCapacity(rawRead, "Read", source, { appliedUnits: DEFAULT_CAPACITY_UNITS }, diagnostics, { blockName: "Replicas[local].ReadProvisionedThroughputSettings" });
13873
+ if (write === void 0) reportUnresolvedProvisionedCapacity(rawWrite, "Write", source, { appliedUnits: DEFAULT_CAPACITY_UNITS }, diagnostics, { blockName: "WriteProvisionedThroughputSettings" });
13813
13874
  return {
13814
- ReadCapacityUnits: deriveReadCapacityUnits(asRecord$1((properties["Replicas"] ?? []).find((r) => r["Region"] === region)?.["ReadProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS,
13815
- WriteCapacityUnits: deriveWriteCapacityUnits(asRecord$1(properties["WriteProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS
13875
+ ReadCapacityUnits: read ?? DEFAULT_CAPACITY_UNITS,
13876
+ WriteCapacityUnits: write ?? DEFAULT_CAPACITY_UNITS
13816
13877
  };
13817
13878
  }
13818
13879
  /**
@@ -13967,6 +14028,26 @@ function asRecord$1(value) {
13967
14028
  * read-back comparison must therefore expect ABSENCE, not this value.
13968
14029
  */
13969
14030
  const ON_DEMAND_LIMIT_RESET = -1;
14031
+ /**
14032
+ * The GSI states during which AWS refuses another `UpdateTable` or a
14033
+ * `DeleteTable` on the table (issue #1521) — a table is ACTIVE while one of
14034
+ * its indexes is still transitioning, which is what made the serialized
14035
+ * update sequence race and what left a table ORPHANED when the follow-up
14036
+ * destroy hit the same rule.
14037
+ *
14038
+ * `DELETING` is included because a dropped index keeps reporting until it is
14039
+ * actually gone. The test is on the TRANSITIONAL values rather than on
14040
+ * `!== 'ACTIVE'`: an absent or unrecognized status must not park the caller
14041
+ * for its whole cap on a table that is fine.
14042
+ */
14043
+ const INDEX_TRANSITIONAL_STATUSES = /* @__PURE__ */ new Set([
14044
+ "CREATING",
14045
+ "UPDATING",
14046
+ "DELETING"
14047
+ ]);
14048
+ function hasTransitionalIndex(indexes) {
14049
+ return (indexes ?? []).some((gsi) => INDEX_TRANSITIONAL_STATUSES.has(gsi.IndexStatus ?? ""));
14050
+ }
13970
14051
  /** Coerce a CFn numeric (CFn is stringly-typed) to a finite number. */
13971
14052
  function toFiniteNumber(value) {
13972
14053
  if (value === void 0 || value === null || value === "") return void 0;
@@ -14057,6 +14138,94 @@ function reportUnresolvedRawMember(rawValue, diagnostics, context) {
14057
14138
  });
14058
14139
  }
14059
14140
  /**
14141
+ * Which member of a `*ProvisionedThroughputSettings` block to blame when the
14142
+ * derivation came back `undefined` (issue #1511).
14143
+ *
14144
+ * The derivation reports only "no number", so the member is re-derived here in
14145
+ * the SAME order {@link deriveWriteCapacityUnits} / {@link deriveReadCapacityUnits}
14146
+ * / {@link pickAutoScalingCapacity} consult members — the literal first, then
14147
+ * the auto-scaling block's `source`-selected member. Reaching this function at
14148
+ * all means every one of them failed, so the first DECLARED one is the value
14149
+ * the user was trying to set.
14150
+ *
14151
+ * `relPath` is relative to the block, `''` meaning the block ITSELF is the
14152
+ * unusable value (a string / array / bare unresolved intrinsic, or an object
14153
+ * carrying no capacity member at all).
14154
+ *
14155
+ * Returns `undefined` for an ABSENT block: defaulting there is the documented
14156
+ * pre-existing behavior for a template that never asked for a capacity, and
14157
+ * warning about it would shout on every deploy of a table that is fine.
14158
+ */
14159
+ function blameUnresolvedProvisionedMember(rawSettings, side, source) {
14160
+ if (rawSettings === void 0) return void 0;
14161
+ const settings = asRecord$1(rawSettings);
14162
+ if (!settings || isUnresolvedIntrinsicBlock(settings)) return {
14163
+ relPath: "",
14164
+ value: rawSettings
14165
+ };
14166
+ const literal = settings[`${side}CapacityUnits`];
14167
+ if (literal !== void 0) return {
14168
+ relPath: `${side}CapacityUnits`,
14169
+ value: literal
14170
+ };
14171
+ const autoKey = `${side}CapacityAutoScalingSettings`;
14172
+ const rawAutoScaling = settings[autoKey];
14173
+ if (rawAutoScaling === void 0) return {
14174
+ relPath: "",
14175
+ value: rawSettings
14176
+ };
14177
+ const autoScaling = asRecord$1(rawAutoScaling);
14178
+ if (!autoScaling || isUnresolvedIntrinsicBlock(autoScaling)) return {
14179
+ relPath: autoKey,
14180
+ value: rawAutoScaling
14181
+ };
14182
+ const order = source === "seed" ? ["SeedCapacity", "MinCapacity"] : ["MinCapacity", "SeedCapacity"];
14183
+ for (const member of order) {
14184
+ const value = autoScaling[member];
14185
+ if (value !== void 0) return {
14186
+ relPath: `${autoKey}.${member}`,
14187
+ value
14188
+ };
14189
+ }
14190
+ return {
14191
+ relPath: autoKey,
14192
+ value: rawAutoScaling
14193
+ };
14194
+ }
14195
+ /**
14196
+ * Report a DECLARED provisioned capacity the template could not resolve — the
14197
+ * PROVISIONED twin of {@link reportUnresolvedRawMember} (issue #1511).
14198
+ *
14199
+ * The two sides need different wording because they have different outcomes,
14200
+ * which is exactly why #1503 deferred this half. An unreadable ON-DEMAND
14201
+ * ceiling is SUPPRESSED (nothing is sent, the live ceiling stands), so its
14202
+ * diagnostic says "nothing was sent". A provisioned table cannot suppress —
14203
+ * AWS requires `ProvisionedThroughput` on the table and on every GSI of it —
14204
+ * so the derivation falls through to {@link DEFAULT_CAPACITY_UNITS} and a
14205
+ * table the template explicitly sized is deployed at 5/5. That is a throttling
14206
+ * symptom days later rather than an error, which is what makes naming it here
14207
+ * worth a warning at all. The one provisioned site that CAN suppress is a
14208
+ * replica's `ProvisionedThroughputOverride` (absent = inherit the source
14209
+ * table), hence the `suppressed` outcome arm.
14210
+ *
14211
+ * Caller decides WHEN: only when the derivation returned `undefined` AND no
14212
+ * explicit SDK-shaped member is standing in for it, so the warning always
14213
+ * describes what actually reached AWS.
14214
+ */
14215
+ function reportUnresolvedProvisionedCapacity(rawSettings, side, source, outcome, diagnostics, context) {
14216
+ if (!diagnostics) return;
14217
+ const blame = blameUnresolvedProvisionedMember(rawSettings, side, source);
14218
+ if (!blame) return;
14219
+ const path = blame.relPath ? `${context.blockName}.${blame.relPath}` : context.blockName;
14220
+ const consequence = "appliedUnits" in outcome ? `so cdkd sent ${outcome.appliedUnits} capacity units instead (its default — AWS requires a concrete provisioned capacity here, so there was nothing to suppress)` : outcome.suppressed;
14221
+ diagnostics.push({
14222
+ kind: "unresolved-member",
14223
+ ...context.indexName !== void 0 && { indexName: context.indexName },
14224
+ member: blame.relPath ? blame.relPath.slice(blame.relPath.lastIndexOf(".") + 1) : context.blockName.slice(context.blockName.lastIndexOf(".") + 1),
14225
+ message: `${path} is declared but did not resolve to a number (${JSON.stringify(blame.value)?.slice(0, 80)}), ${consequence}. This is usually an unresolved intrinsic; resolve it to a literal to apply the capacity you declared.`
14226
+ });
14227
+ }
14228
+ /**
14060
14229
  * Derive a single WRITE capacity number from a CFn
14061
14230
  * `WriteProvisionedThroughputSettings` block.
14062
14231
  *
@@ -14187,14 +14356,32 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "
14187
14356
  member: "OnDemandThroughput",
14188
14357
  message: "an explicit OnDemandThroughput block was dropped because the table's BillingMode is PROVISIONED; AWS rejects on-demand ceilings on a provisioned table. Remove the block, or set BillingMode: PAY_PER_REQUEST."
14189
14358
  });
14190
- if (billingMode === "PROVISIONED") sdk.ProvisionedThroughput = mergeExplicitThroughputBlock(explicitProvisioned, {
14191
- ReadCapacityUnits: deriveReadCapacityUnits(asRecord$1(localEntry?.["ReadProvisionedThroughputSettings"]), source) ?? deriveReadCapacityUnits(asRecord$1(gsi["ReadProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS,
14192
- WriteCapacityUnits: deriveWriteCapacityUnits(asRecord$1(gsi["WriteProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS
14193
- }, ["ReadCapacityUnits", "WriteCapacityUnits"], diagnostics, {
14194
- ...typeof indexName === "string" && { indexName },
14195
- blockName: "ProvisionedThroughput"
14196
- });
14197
- else {
14359
+ if (billingMode === "PROVISIONED") {
14360
+ const rawLocalRead = localEntry?.["ReadProvisionedThroughputSettings"];
14361
+ const rawGsiRead = gsi["ReadProvisionedThroughputSettings"];
14362
+ const rawWrite = gsi["WriteProvisionedThroughputSettings"];
14363
+ const derivedRead = deriveReadCapacityUnits(asRecord$1(rawLocalRead), source) ?? deriveReadCapacityUnits(asRecord$1(rawGsiRead), source);
14364
+ const derivedWrite = deriveWriteCapacityUnits(asRecord$1(rawWrite), source);
14365
+ const explicitCovers = (member) => explicitProvisioned !== void 0 && toFiniteNumber(explicitProvisioned[member]) !== void 0;
14366
+ if (derivedRead === void 0 && !explicitCovers("ReadCapacityUnits")) {
14367
+ const fromReplica = rawLocalRead !== void 0;
14368
+ reportUnresolvedProvisionedCapacity(fromReplica ? rawLocalRead : rawGsiRead, "Read", source, { appliedUnits: DEFAULT_CAPACITY_UNITS }, diagnostics, {
14369
+ ...typeof indexName === "string" && { indexName },
14370
+ blockName: fromReplica ? "Replicas[local].GlobalSecondaryIndexes[].ReadProvisionedThroughputSettings" : "ReadProvisionedThroughputSettings"
14371
+ });
14372
+ }
14373
+ if (derivedWrite === void 0 && !explicitCovers("WriteCapacityUnits")) reportUnresolvedProvisionedCapacity(rawWrite, "Write", source, { appliedUnits: DEFAULT_CAPACITY_UNITS }, diagnostics, {
14374
+ ...typeof indexName === "string" && { indexName },
14375
+ blockName: "WriteProvisionedThroughputSettings"
14376
+ });
14377
+ sdk.ProvisionedThroughput = mergeExplicitThroughputBlock(explicitProvisioned, {
14378
+ ReadCapacityUnits: derivedRead ?? DEFAULT_CAPACITY_UNITS,
14379
+ WriteCapacityUnits: derivedWrite ?? DEFAULT_CAPACITY_UNITS
14380
+ }, ["ReadCapacityUnits", "WriteCapacityUnits"], diagnostics, {
14381
+ ...typeof indexName === "string" && { indexName },
14382
+ blockName: "ProvisionedThroughput"
14383
+ });
14384
+ } else {
14198
14385
  const rawWrite = asRecord$1(gsi["WriteOnDemandThroughputSettings"])?.["MaxWriteRequestUnits"];
14199
14386
  const rawReadLocal = asRecord$1(localEntry?.["ReadOnDemandThroughputSettings"])?.["MaxReadRequestUnits"];
14200
14387
  const rawReadGsi = asRecord$1(gsi["ReadOnDemandThroughputSettings"])?.["MaxReadRequestUnits"];
@@ -14363,7 +14550,12 @@ function toSdkReplicaThroughputOverrides(cfn, billingMode, diagnostics, context)
14363
14550
  message: "an explicit OnDemandThroughputOverride was dropped because the table's BillingMode is PROVISIONED; AWS rejects on-demand ceilings on a provisioned table."
14364
14551
  });
14365
14552
  if (billingMode === "PROVISIONED") {
14366
- const readCapacity = deriveReadCapacityUnits(asRecord$1(cfn["ReadProvisionedThroughputSettings"]));
14553
+ const rawRead = cfn["ReadProvisionedThroughputSettings"];
14554
+ const readCapacity = deriveReadCapacityUnits(asRecord$1(rawRead));
14555
+ if (readCapacity === void 0 && toFiniteNumber(explicitProvisioned?.["ReadCapacityUnits"]) === void 0) reportUnresolvedProvisionedCapacity(rawRead, "Read", "min", { suppressed: "so no ProvisionedThroughputOverride was sent and the replica keeps the source table's read capacity" }, diagnostics, {
14556
+ ...context,
14557
+ blockName: "ReadProvisionedThroughputSettings"
14558
+ });
14367
14559
  const merged = mergeExplicitThroughputBlock(explicitProvisioned, readCapacity !== void 0 ? { ReadCapacityUnits: readCapacity } : void 0, ["ReadCapacityUnits"], diagnostics, {
14368
14560
  ...context,
14369
14561
  blockName: "ProvisionedThroughputOverride"
@@ -43910,9 +44102,11 @@ var S3DirectoryBucketProvider = class {
43910
44102
  handledProperties = /* @__PURE__ */ new Map([["AWS::S3Express::DirectoryBucket", /* @__PURE__ */ new Set([
43911
44103
  "DataRedundancy",
43912
44104
  "LocationName",
43913
- "BucketName"
44105
+ "BucketName",
44106
+ "Tags"
43914
44107
  ])]]);
43915
44108
  ec2Client;
44109
+ s3ControlClient;
43916
44110
  providerRegion = process.env["AWS_REGION"];
43917
44111
  constructor() {
43918
44112
  const awsClients = getAwsClients();
@@ -43924,6 +44118,17 @@ var S3DirectoryBucketProvider = class {
43924
44118
  return this.ec2Client;
43925
44119
  }
43926
44120
  /**
44121
+ * Directory-bucket tags live on the S3 control plane: `PutBucketTagging`
44122
+ * is documented "not supported for directory buckets", and the tag CRUD
44123
+ * after create is S3 Control's `TagResource` / `UntagResource` /
44124
+ * `ListTagsForResource` (requiring the `s3express:TagResource` /
44125
+ * `s3express:UntagResource` / `s3express:ListTagsForResource` permissions).
44126
+ */
44127
+ getS3ControlClient() {
44128
+ if (!this.s3ControlClient) this.s3ControlClient = new S3ControlClient(this.providerRegion ? { region: this.providerRegion } : {});
44129
+ return this.s3ControlClient;
44130
+ }
44131
+ /**
43927
44132
  * Convert AZ name (us-east-1a) to AZ ID (use1-az1) via EC2 DescribeAvailabilityZones
43928
44133
  */
43929
44134
  async getAzId(azName) {
@@ -43954,7 +44159,16 @@ var S3DirectoryBucketProvider = class {
43954
44159
  * Build attributes for a directory bucket
43955
44160
  */
43956
44161
  async buildAttributes(bucketName) {
43957
- return { Arn: `arn:aws:s3express:${await this.getRegion()}:${await this.getAccountId()}:bucket/${bucketName}` };
44162
+ return { Arn: await this.buildBucketArn(bucketName) };
44163
+ }
44164
+ /**
44165
+ * Directory bucket ARN: `arn:aws:s3express:{region}:{account}:bucket/{name}`.
44166
+ * Also the `ResourceArn` for the S3 Control tag operations. Callers that
44167
+ * already resolved the account (for the S3 Control `AccountId` parameter)
44168
+ * pass it in so the STS lookup runs once per operation.
44169
+ */
44170
+ async buildBucketArn(bucketName, accountId) {
44171
+ return `arn:aws:s3express:${await this.getRegion()}:${accountId ?? await this.getAccountId()}:bucket/${bucketName}`;
43958
44172
  }
43959
44173
  /**
43960
44174
  * Create an S3 Express Directory Bucket
@@ -43969,6 +44183,8 @@ var S3DirectoryBucketProvider = class {
43969
44183
  maxLength: 64,
43970
44184
  lowercase: true
43971
44185
  })}--${azId}--x-s3`;
44186
+ const tags = properties["Tags"];
44187
+ const includeTags = tags != null && !(Array.isArray(tags) && tags.length === 0);
43972
44188
  try {
43973
44189
  await this.s3Client.send(new CreateBucketCommand({
43974
44190
  Bucket: bucketName,
@@ -43980,7 +44196,8 @@ var S3DirectoryBucketProvider = class {
43980
44196
  Location: {
43981
44197
  Name: azId,
43982
44198
  Type: "AvailabilityZone"
43983
- }
44199
+ },
44200
+ ...includeTags ? { Tags: tags } : {}
43984
44201
  }
43985
44202
  }));
43986
44203
  this.logger.debug(`Created S3 Express Directory Bucket: ${bucketName}`);
@@ -43997,28 +44214,60 @@ var S3DirectoryBucketProvider = class {
43997
44214
  /**
43998
44215
  * Update an S3 Express Directory Bucket
43999
44216
  *
44000
- * Most properties are immutable, so this is a no-op.
44217
+ * `BucketName` / `DataRedundancy` / `LocationName` are create-only (the
44218
+ * registry-schema fallback classifies changes to them as replacement), so
44219
+ * the only in-place mutable property is `Tags` — applied via S3 Control's
44220
+ * `TagResource` / `UntagResource` (the directory-bucket tag CRUD; the
44221
+ * bucket-level `PutBucketTagging` is not supported for directory buckets).
44222
+ * `TagResource` is additive-only, so a tag dropped from the template
44223
+ * (partial removal) — or the entire `Tags` property removed (full removal,
44224
+ * `newTags === undefined`) — would survive on AWS unless the removed keys
44225
+ * are explicitly `UntagResource`d.
44001
44226
  */
44002
- update(logicalId, physicalId, _resourceType, _properties, _previousProperties) {
44003
- this.logger.debug(`Update for S3 Express Directory Bucket ${logicalId} is a no-op (immutable properties)`);
44004
- return Promise.resolve({
44005
- physicalId,
44006
- wasReplaced: false
44007
- });
44227
+ async update(logicalId, physicalId, resourceType, properties, previousProperties) {
44228
+ try {
44229
+ const newTags = properties["Tags"];
44230
+ const oldTags = previousProperties["Tags"];
44231
+ const malformedNewTags = newTags != null && !Array.isArray(newTags);
44232
+ const newKeys = new Set((Array.isArray(newTags) ? newTags : []).map((t) => t.Key).filter((k) => !!k));
44233
+ const removedKeys = malformedNewTags ? [] : (Array.isArray(oldTags) ? oldTags : []).map((t) => t.Key).filter((k) => !!k && !newKeys.has(k));
44234
+ const tagsToApply = malformedNewTags || Array.isArray(newTags) && newTags.length > 0 ? newTags : void 0;
44235
+ if (JSON.stringify(newTags) !== JSON.stringify(oldTags) && (removedKeys.length > 0 || tagsToApply !== void 0)) {
44236
+ const accountId = await this.getAccountId();
44237
+ const resourceArn = await this.buildBucketArn(physicalId, accountId);
44238
+ if (removedKeys.length > 0) await this.getS3ControlClient().send(new UntagResourceCommand$23({
44239
+ AccountId: accountId,
44240
+ ResourceArn: resourceArn,
44241
+ TagKeys: removedKeys
44242
+ }));
44243
+ if (tagsToApply) await this.getS3ControlClient().send(new TagResourceCommand$23({
44244
+ AccountId: accountId,
44245
+ ResourceArn: resourceArn,
44246
+ Tags: tagsToApply
44247
+ }));
44248
+ this.logger.debug(`Updated tags for S3 Express Directory Bucket ${physicalId}`);
44249
+ } else this.logger.debug(`Update for S3 Express Directory Bucket ${logicalId} is a no-op (no tag changes; other properties are immutable)`);
44250
+ return {
44251
+ physicalId,
44252
+ wasReplaced: false
44253
+ };
44254
+ } catch (error) {
44255
+ const cause = error instanceof Error ? error : void 0;
44256
+ throw new ProvisioningError(`Failed to update S3 Express Directory Bucket ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
44257
+ }
44008
44258
  }
44009
44259
  /**
44010
44260
  * Delete an S3 Express Directory Bucket
44011
44261
  *
44012
44262
  * CloudFormation-parity data guard (issue #1344, sibling of #1340): a
44013
44263
  * non-empty directory bucket is only auto-emptied when the user opted in —
44014
- * the `aws-cdk:auto-delete-objects` tag on the bucket (settable only via
44015
- * `--allow-unsupported-properties` today, since `Tags` is not yet a
44016
- * handled property for this type) or the deploy engine's
44017
- * `--force-stateful-recreation` consent on a replacement delete
44018
- * (`DeleteContext.forceDataDelete`). Otherwise the not-empty error
44019
- * surfaces exactly like CloudFormation's DELETE_FAILED (live-A/B-verified
44020
- * 2026-08-03: CFn fails a non-empty DirectoryBucket delete with "The
44021
- * bucket you tried to delete is not empty", 409).
44264
+ * the `aws-cdk:auto-delete-objects` tag on the bucket (a first-class
44265
+ * template `Tags` entry now that `Tags` is a handled property, issue #609)
44266
+ * or the deploy engine's `--force-stateful-recreation` consent on a
44267
+ * replacement delete (`DeleteContext.forceDataDelete`). Otherwise the
44268
+ * not-empty error surfaces exactly like CloudFormation's DELETE_FAILED
44269
+ * (live-A/B-verified 2026-08-03: CFn fails a non-empty DirectoryBucket
44270
+ * delete with "The bucket you tried to delete is not empty", 409).
44022
44271
  *
44023
44272
  * Directory buckets do not support versioning, so the opted-in auto-empty
44024
44273
  * only needs to delete current objects.
@@ -44027,15 +44276,8 @@ var S3DirectoryBucketProvider = class {
44027
44276
  this.logger.debug(`Deleting S3 Express Directory Bucket ${logicalId}: ${physicalId}`);
44028
44277
  const allowAutoEmpty = context?.forceDataDelete === true || hasCdkAutoDeleteTag(properties, "aws-cdk:auto-delete-objects");
44029
44278
  try {
44030
- if (allowAutoEmpty) {
44031
- this.logger.info(`Emptying directory bucket ${physicalId} before deletion (auto-delete opt-in present)`);
44032
- await this.emptyBucket(physicalId);
44033
- }
44034
- await this.s3Client.send(new DeleteBucketCommand({ Bucket: physicalId }));
44035
- this.logger.debug(`Successfully deleted S3 Express Directory Bucket ${logicalId}`);
44279
+ await this.deleteBucketWithEmptyRetry(logicalId, physicalId, allowAutoEmpty);
44036
44280
  } catch (error) {
44037
- const notEmptyMsg = error instanceof Error ? error.message : String(error);
44038
- if (!allowAutoEmpty && (notEmptyMsg.includes("not empty") || notEmptyMsg.includes("BucketNotEmpty"))) throw new ProvisioningError(`Failed to delete S3 Express Directory Bucket ${logicalId}: bucket ${physicalId} is not empty. Matching CloudFormation, cdkd does not delete a non-empty directory bucket without an explicit opt-in. Delete all objects first (e.g. aws s3 rm s3://${physicalId} --recursive) and destroy again.`, resourceType, logicalId, physicalId, error instanceof Error ? error : void 0);
44039
44281
  if (error instanceof Error && (error.name === "NoSuchBucket" || error.name === "BucketNotFound")) {
44040
44282
  assertRegionMatch(await this.s3Client.config.region(), context?.expectedRegion, resourceType, logicalId, physicalId);
44041
44283
  this.logger.debug(`Bucket ${physicalId} does not exist, skipping deletion`);
@@ -44046,6 +44288,35 @@ var S3DirectoryBucketProvider = class {
44046
44288
  }
44047
44289
  }
44048
44290
  /**
44291
+ * Delete the bucket, absorbing the write race on an opted-in auto-empty:
44292
+ * an object written between the empty pass and the delete makes AWS reject
44293
+ * the delete as not-empty, so re-empty and retry (bounded) — the same
44294
+ * bounded loop as the standard-bucket sibling, ported per issue #609's
44295
+ * "Tags becomes a handled property" note (the tag opt-in is mainstream
44296
+ * now, so the ~zero-opt-in rationale for skipping the loop in PR #1347 no
44297
+ * longer holds). Without the opt-in, the first not-empty error surfaces
44298
+ * as the CloudFormation-parity guard (issue #1344).
44299
+ */
44300
+ async deleteBucketWithEmptyRetry(logicalId, bucketName, allowAutoEmpty) {
44301
+ const maxAttempts = 3;
44302
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
44303
+ await this.s3Client.send(new DeleteBucketCommand({ Bucket: bucketName }));
44304
+ this.logger.debug(`Successfully deleted S3 Express Directory Bucket ${logicalId}`);
44305
+ return;
44306
+ } catch (error) {
44307
+ const msg = error instanceof Error ? error.message : String(error);
44308
+ if (msg.includes("not empty") || msg.includes("BucketNotEmpty")) {
44309
+ if (!allowAutoEmpty) throw new Error(`bucket ${bucketName} is not empty. Matching CloudFormation, cdkd does not delete a non-empty directory bucket without an explicit opt-in. Delete all objects first (e.g. aws s3 rm s3://${bucketName} --recursive) and destroy again.`);
44310
+ this.logger.info(`Directory bucket ${bucketName} not empty (attempt ${attempt}/${maxAttempts}), emptying (auto-delete opt-in present)...`);
44311
+ await this.emptyBucket(bucketName);
44312
+ continue;
44313
+ }
44314
+ throw error;
44315
+ }
44316
+ await this.s3Client.send(new DeleteBucketCommand({ Bucket: bucketName }));
44317
+ this.logger.debug(`Successfully deleted S3 Express Directory Bucket ${logicalId}`);
44318
+ }
44319
+ /**
44049
44320
  * Empty all objects from a directory bucket.
44050
44321
  * Lists and deletes objects in batches of 1000.
44051
44322
  */
@@ -44076,10 +44347,11 @@ var S3DirectoryBucketProvider = class {
44076
44347
  * CFn-property shape.
44077
44348
  *
44078
44349
  * Issues `HeadBucket` to verify existence and surfaces `BucketName`
44079
- * (the physicalId). `DataRedundancy` and `LocationName` are not exposed
44080
- * by any cheap S3 Express API — the only way to inspect them is by
44081
- * parsing the bucket name's `--<az-id>--x-s3` suffix, which is best
44082
- * effort. We surface them when they are recoverable from the bucket
44350
+ * (the physicalId) plus `Tags` (via S3 Control `ListTagsForResource`,
44351
+ * the directory-bucket tag read). `DataRedundancy` and `LocationName`
44352
+ * are not exposed by any cheap S3 Express API — the only way to inspect
44353
+ * them is by parsing the bucket name's `--<az-id>--x-s3` suffix, which is
44354
+ * best effort. We surface them when they are recoverable from the bucket
44083
44355
  * name to give the comparator a chance to detect mismatches.
44084
44356
  *
44085
44357
  * `--<az-id>--x-s3` suffix parsing: directory bucket names follow the
@@ -44105,8 +44377,17 @@ var S3DirectoryBucketProvider = class {
44105
44377
  }
44106
44378
  const result = { BucketName: physicalId };
44107
44379
  if (physicalId.endsWith("--x-s3")) result["DataRedundancy"] = "SingleAvailabilityZone";
44380
+ result["Tags"] = await this.readTags(physicalId);
44108
44381
  return result;
44109
44382
  }
44383
+ async readTags(bucketName) {
44384
+ const accountId = await this.getAccountId();
44385
+ const resourceArn = await this.buildBucketArn(bucketName, accountId);
44386
+ return normalizeAwsTagsToCfn((await this.getS3ControlClient().send(new ListTagsForResourceCommand$18({
44387
+ AccountId: accountId,
44388
+ ResourceArn: resourceArn
44389
+ }))).Tags);
44390
+ }
44110
44391
  /**
44111
44392
  * Adopt an existing S3 Express Directory Bucket into cdkd state.
44112
44393
  *
@@ -44592,7 +44873,7 @@ var S3TablesProvider = class {
44592
44873
  */
44593
44874
  async readTagsBestEffort(resourceArn) {
44594
44875
  try {
44595
- const tags = (await this.getClient().send(new ListTagsForResourceCommand$18({ resourceArn }))).tags ?? {};
44876
+ const tags = (await this.getClient().send(new ListTagsForResourceCommand$19({ resourceArn }))).tags ?? {};
44596
44877
  const out = [];
44597
44878
  for (const [Key, Value] of Object.entries(tags)) out.push({
44598
44879
  Key,
@@ -44646,7 +44927,7 @@ var S3TablesProvider = class {
44646
44927
  const resourceArn = await this.lookupTableArn(tableBucketARN, namespace, name);
44647
44928
  if (!resourceArn) throw new ProvisioningError(`applyTableTagsDiff: GetTable returned no tableARN for ${physicalId} — table is gone or state is out-of-sync. Refusing to silently drop the tag update (run 'cdkd state orphan ${logicalId}' to clean up if the table was deleted out-of-band).`, resourceType, logicalId, physicalId);
44648
44929
  if (removedKeys.length > 0) try {
44649
- await this.getClient().send(new UntagResourceCommand$23({
44930
+ await this.getClient().send(new UntagResourceCommand$24({
44650
44931
  resourceArn,
44651
44932
  tagKeys: removedKeys
44652
44933
  }));
@@ -44655,7 +44936,7 @@ var S3TablesProvider = class {
44655
44936
  throw new ProvisioningError(`applyTableTagsDiff: UntagResource failed for ${resourceArn} (keys: ${removedKeys.join(", ")}): ${err instanceof Error ? err.message : String(err)}. State has NOT been updated so the next deploy will retry the tag-diff.`, resourceType, logicalId, physicalId, cause);
44656
44937
  }
44657
44938
  if (Object.keys(upserts).length > 0) try {
44658
- await this.getClient().send(new TagResourceCommand$23({
44939
+ await this.getClient().send(new TagResourceCommand$24({
44659
44940
  resourceArn,
44660
44941
  tags: upserts
44661
44942
  }));
@@ -44687,7 +44968,7 @@ var S3TablesProvider = class {
44687
44968
  if (removedKeys.length === 0 && Object.keys(upserts).length === 0) return;
44688
44969
  const resourceArn = physicalId;
44689
44970
  if (removedKeys.length > 0) try {
44690
- await this.getClient().send(new UntagResourceCommand$23({
44971
+ await this.getClient().send(new UntagResourceCommand$24({
44691
44972
  resourceArn,
44692
44973
  tagKeys: removedKeys
44693
44974
  }));
@@ -44696,7 +44977,7 @@ var S3TablesProvider = class {
44696
44977
  throw new ProvisioningError(`applyTableBucketTagsDiff: UntagResource failed for ${resourceArn} (keys: ${removedKeys.join(", ")}): ${err instanceof Error ? err.message : String(err)}. State has NOT been updated so the next deploy will retry the tag-diff.`, resourceType, logicalId, physicalId, cause);
44697
44978
  }
44698
44979
  if (Object.keys(upserts).length > 0) try {
44699
- await this.getClient().send(new TagResourceCommand$23({
44980
+ await this.getClient().send(new TagResourceCommand$24({
44700
44981
  resourceArn,
44701
44982
  tags: upserts
44702
44983
  }));
@@ -66616,7 +66897,7 @@ function createMigrateCommand() {
66616
66897
  */
66617
66898
  function buildProgram() {
66618
66899
  const program = new Command();
66619
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.35");
66900
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.37");
66620
66901
  program.hook("preAction", (_thisCommand, actionCommand) => {
66621
66902
  const { profile } = actionCommand.optsWithGlobals();
66622
66903
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;