@go-to-k/cdkd 0.278.36 → 0.278.38

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,11 +1,11 @@
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-iHGY-Q_U.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-DIvnBeMj.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-5hYG8VHd.js";
4
+ import { t as ASGProvider } from "./asg-provider-CaulrRij.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";
8
- import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachRolePolicyCommand, AttachUserPolicyCommand, CreateAccessKeyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreatePolicyCommand, CreatePolicyVersionCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeletePolicyCommand, DeletePolicyVersionCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachRolePolicyCommand, DetachUserPolicyCommand, GetAccessKeyLastUsedCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetPolicyCommand, GetPolicyVersionCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListEntitiesForPolicyCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListPoliciesCommand, ListPolicyTagsCommand, ListPolicyVersionsCommand, ListUserPoliciesCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagPolicyCommand, TagUserCommand, UntagPolicyCommand, UntagUserCommand, UpdateAccessKeyCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
8
+ import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachRolePolicyCommand, AttachUserPolicyCommand, CreateAccessKeyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreatePolicyCommand, CreatePolicyVersionCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeletePolicyCommand, DeletePolicyVersionCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachRolePolicyCommand, DetachUserPolicyCommand, GetAccessKeyLastUsedCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetPolicyCommand, GetPolicyVersionCommand, GetRoleCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListEntitiesForPolicyCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListPoliciesCommand, ListPolicyTagsCommand, ListPolicyVersionsCommand, ListUserPoliciesCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagPolicyCommand, TagUserCommand, UntagPolicyCommand, UntagUserCommand, UpdateAccessKeyCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
9
9
  import { CreateQueueCommand, DeleteQueueCommand, GetQueueAttributesCommand, GetQueueUrlCommand, ListQueueTagsCommand, QueueDoesNotExist, SQSClient, SetQueueAttributesCommand, TagQueueCommand, UntagQueueCommand } from "@aws-sdk/client-sqs";
10
10
  import { CreateTopicCommand, DeleteTopicCommand, GetSubscriptionAttributesCommand, GetTopicAttributesCommand, InvalidParameterException, ListSubscriptionsByTopicCommand, ListTagsForResourceCommand, ListTopicsCommand, NotFoundException, SNSClient, SetTopicAttributesCommand, SubscribeCommand, TagResourceCommand, UnsubscribeCommand, UntagResourceCommand } from "@aws-sdk/client-sns";
11
11
  import { AddPermissionCommand, CreateEventSourceMappingCommand, CreateFunctionCommand, CreateFunctionUrlConfigCommand, DeleteEventSourceMappingCommand, DeleteFunctionCommand, DeleteFunctionConcurrencyCommand, DeleteFunctionEventInvokeConfigCommand, DeleteFunctionUrlConfigCommand, DeleteLayerVersionCommand, GetEventSourceMappingCommand, GetFunctionCommand, GetFunctionConcurrencyCommand, GetFunctionEventInvokeConfigCommand, GetFunctionRecursionConfigCommand, GetFunctionUrlConfigCommand, GetLayerVersionByArnCommand, GetPolicyCommand as GetPolicyCommand$1, LambdaClient, ListTagsCommand, PublishLayerVersionCommand, PutFunctionConcurrencyCommand, PutFunctionEventInvokeConfigCommand, PutFunctionRecursionConfigCommand, RemovePermissionCommand, ResourceNotFoundException, TagResourceCommand as TagResourceCommand$1, UntagResourceCommand as UntagResourceCommand$1, UpdateEventSourceMappingCommand, UpdateFunctionCodeCommand, UpdateFunctionConfigurationCommand, UpdateFunctionUrlConfigCommand, waitUntilFunctionUpdatedV2 } from "@aws-sdk/client-lambda";
@@ -12315,7 +12315,8 @@ var DynamoDBGlobalTableProvider = class {
12315
12315
  AttributeDefinitions: attributeDefinitions,
12316
12316
  BillingMode: billingMode
12317
12317
  };
12318
- if (billingMode === "PROVISIONED") createParams.ProvisionedThroughput = derivePerCallProvisionedThroughput(properties, currentRegion);
12318
+ const diagnostics = [];
12319
+ if (billingMode === "PROVISIONED") createParams.ProvisionedThroughput = derivePerCallProvisionedThroughput(properties, currentRegion, "min", diagnostics);
12319
12320
  const streamSpecInput = properties["StreamSpecification"];
12320
12321
  const needsStream = replicas.some((r) => r["Region"] !== currentRegion) || replicas.length > 1;
12321
12322
  if (streamSpecInput != null) {
@@ -12336,7 +12337,6 @@ var DynamoDBGlobalTableProvider = class {
12336
12337
  StreamViewType: "NEW_AND_OLD_IMAGES"
12337
12338
  };
12338
12339
  }
12339
- const diagnostics = [];
12340
12340
  let sdkIndexes;
12341
12341
  try {
12342
12342
  sdkIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, billingMode, "min", diagnostics);
@@ -12552,7 +12552,10 @@ var DynamoDBGlobalTableProvider = class {
12552
12552
  const onDemandCeilingLive = oldBilling === "PAY_PER_REQUEST" && newBilling === "PAY_PER_REQUEST";
12553
12553
  const diagnostics = [];
12554
12554
  const desiredCeilings = collectTableOnDemandCeilings(properties, currentRegion, diagnostics);
12555
- 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);
12556
12559
  for (const replica of properties["Replicas"] ?? []) {
12557
12560
  const replicaRegion = asRecord$1(replica)?.["Region"];
12558
12561
  if (typeof replicaRegion !== "string" || replicaRegion === currentRegion) continue;
@@ -13232,9 +13235,11 @@ var DynamoDBGlobalTableProvider = class {
13232
13235
  const cause = regionErr instanceof Error ? regionErr : void 0;
13233
13236
  throw new ProvisioningError(`Could not resolve client region for DynamoDB GlobalTable ${logicalId} delete — would risk dropping the local replica`, resourceType, logicalId, physicalId, cause);
13234
13237
  }
13238
+ let preDeleteIndexes;
13235
13239
  try {
13236
13240
  const describe = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }));
13237
13241
  const replicas = describe.Table?.Replicas ?? [];
13242
+ preDeleteIndexes = describe.Table?.GlobalSecondaryIndexes;
13238
13243
  const tableIndexNames = (describe.Table?.GlobalSecondaryIndexes ?? []).map((gsi) => gsi.IndexName).filter((name) => typeof name === "string");
13239
13244
  for (const replica of replicas) {
13240
13245
  const region = replica.RegionName;
@@ -13265,6 +13270,10 @@ var DynamoDBGlobalTableProvider = class {
13265
13270
  }
13266
13271
  }
13267
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
+ }
13268
13277
  await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
13269
13278
  await this.waitForTableGone(physicalId, logicalId);
13270
13279
  this.logger.debug(`Successfully deleted DynamoDB GlobalTable ${logicalId}`);
@@ -13679,34 +13688,78 @@ var DynamoDBGlobalTableProvider = class {
13679
13688
  * `waitForTableActive`, this tolerates any non-terminal status — the
13680
13689
  * table may already be ACTIVE on the no-op path (already-disabled
13681
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.
13682
13715
  */
13683
13716
  async waitForTableActiveAfterUpdate(tableName, logicalId, maxAttempts = 600) {
13717
+ let tableReachedActive = false;
13684
13718
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
13685
- 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
+ }
13686
13724
  await new Promise((resolve) => setTimeout(resolve, 1e3));
13687
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
+ }
13688
13730
  throw new ProvisioningError(`Table ${tableName} did not reach ACTIVE within ${maxAttempts}s after UpdateTable`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
13689
13731
  }
13690
13732
  /**
13691
13733
  * Wait until every GSI reports `IndexStatus: ACTIVE` (Issue #1419).
13692
13734
  *
13693
- * `waitForTableActiveAfterUpdate` only checks `TableStatus`, and a GSI
13694
- * added by `UpdateTable` leaves the TABLE ACTIVE while the index itself
13695
- * is still `CREATING` — application-autoscaling rejects a target whose
13696
- * 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
13697
13738
  * would skip the very index the deploy just introduced.
13698
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
+ *
13699
13750
  * **Best-effort by design**: on timeout it warns and returns rather than
13700
13751
  * throwing. A missed registration self-heals on the next deploy (the
13701
13752
  * reconcile re-asserts anything not already present), whereas throwing
13702
13753
  * here would fail a deploy whose actual resources are all correct. Index
13703
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.
13704
13757
  */
13705
13758
  async waitForIndexesActive(tableName, logicalId) {
13706
13759
  const maxAttempts = 900;
13707
13760
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
13708
13761
  try {
13709
- 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;
13710
13763
  } catch (err) {
13711
13764
  this.logger.debug(`DescribeTable while waiting for indexes on ${tableName}: ${err instanceof Error ? err.message : String(err)}`);
13712
13765
  return;
@@ -13810,10 +13863,17 @@ function extractLocalDeletionProtection(props, region) {
13810
13863
  * literal `ReadCapacityUnits`, which is taken before its auto-scaling
13811
13864
  * members; 5 if absent.
13812
13865
  */
13813
- 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" });
13814
13874
  return {
13815
- ReadCapacityUnits: deriveReadCapacityUnits(asRecord$1((properties["Replicas"] ?? []).find((r) => r["Region"] === region)?.["ReadProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS,
13816
- WriteCapacityUnits: deriveWriteCapacityUnits(asRecord$1(properties["WriteProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS
13875
+ ReadCapacityUnits: read ?? DEFAULT_CAPACITY_UNITS,
13876
+ WriteCapacityUnits: write ?? DEFAULT_CAPACITY_UNITS
13817
13877
  };
13818
13878
  }
13819
13879
  /**
@@ -13968,6 +14028,26 @@ function asRecord$1(value) {
13968
14028
  * read-back comparison must therefore expect ABSENCE, not this value.
13969
14029
  */
13970
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
+ }
13971
14051
  /** Coerce a CFn numeric (CFn is stringly-typed) to a finite number. */
13972
14052
  function toFiniteNumber(value) {
13973
14053
  if (value === void 0 || value === null || value === "") return void 0;
@@ -14058,6 +14138,94 @@ function reportUnresolvedRawMember(rawValue, diagnostics, context) {
14058
14138
  });
14059
14139
  }
14060
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
+ /**
14061
14229
  * Derive a single WRITE capacity number from a CFn
14062
14230
  * `WriteProvisionedThroughputSettings` block.
14063
14231
  *
@@ -14188,14 +14356,32 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "
14188
14356
  member: "OnDemandThroughput",
14189
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."
14190
14358
  });
14191
- if (billingMode === "PROVISIONED") sdk.ProvisionedThroughput = mergeExplicitThroughputBlock(explicitProvisioned, {
14192
- ReadCapacityUnits: deriveReadCapacityUnits(asRecord$1(localEntry?.["ReadProvisionedThroughputSettings"]), source) ?? deriveReadCapacityUnits(asRecord$1(gsi["ReadProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS,
14193
- WriteCapacityUnits: deriveWriteCapacityUnits(asRecord$1(gsi["WriteProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS
14194
- }, ["ReadCapacityUnits", "WriteCapacityUnits"], diagnostics, {
14195
- ...typeof indexName === "string" && { indexName },
14196
- blockName: "ProvisionedThroughput"
14197
- });
14198
- 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 {
14199
14385
  const rawWrite = asRecord$1(gsi["WriteOnDemandThroughputSettings"])?.["MaxWriteRequestUnits"];
14200
14386
  const rawReadLocal = asRecord$1(localEntry?.["ReadOnDemandThroughputSettings"])?.["MaxReadRequestUnits"];
14201
14387
  const rawReadGsi = asRecord$1(gsi["ReadOnDemandThroughputSettings"])?.["MaxReadRequestUnits"];
@@ -14364,7 +14550,12 @@ function toSdkReplicaThroughputOverrides(cfn, billingMode, diagnostics, context)
14364
14550
  message: "an explicit OnDemandThroughputOverride was dropped because the table's BillingMode is PROVISIONED; AWS rejects on-demand ceilings on a provisioned table."
14365
14551
  });
14366
14552
  if (billingMode === "PROVISIONED") {
14367
- 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
+ });
14368
14559
  const merged = mergeExplicitThroughputBlock(explicitProvisioned, readCapacity !== void 0 ? { ReadCapacityUnits: readCapacity } : void 0, ["ReadCapacityUnits"], diagnostics, {
14369
14560
  ...context,
14370
14561
  blockName: "ProvisionedThroughputOverride"
@@ -48878,6 +49069,208 @@ function isPlainObject(value) {
48878
49069
  return typeof value === "object" && value !== null;
48879
49070
  }
48880
49071
 
49072
+ //#endregion
49073
+ //#region src/analyzer/drift-principal-normalize.ts
49074
+ /**
49075
+ * IAM principal-form canonicalization for the drift comparator (issue #1515).
49076
+ *
49077
+ * AWS renders an IAM role / user principal inside a resource policy in TWO
49078
+ * forms and picks between them on its own schedule:
49079
+ *
49080
+ * - the ARN — `arn:aws:iam::<acct>:role/<name>`
49081
+ * - the principal's UNIQUE ID — `AROA…` (role) / `AIDA…` (user)
49082
+ *
49083
+ * The unique-id form is substituted when the referenced principal is
49084
+ * transiently unresolvable at write time (it is being created or replaced) and
49085
+ * AWS re-renders the ARN once it resolves. cdkd's deploy-time
49086
+ * `observedProperties` capture stores whichever form the read returned at that
49087
+ * moment, so a later `cdkd drift` can compare `AROA…` against the ARN for the
49088
+ * very same principal and report PERMANENT phantom drift — `--revert` writes
49089
+ * the recorded form back, AWS re-canonicalizes on write, and the next run
49090
+ * reports the identical difference (the class of issues #1096 / #1498, on a
49091
+ * different property).
49092
+ *
49093
+ * Unlike the order-normalization passes in `drift-normalize.ts`, this one
49094
+ * CANNOT be pure: the mapping between the two forms is not derivable from the
49095
+ * string. So the shape detection here is pure and an injected
49096
+ * {@link PrincipalUniqueIdResolver} does the one lookup, which lets the whole
49097
+ * pass be unit-tested without AWS and keeps the API cost at zero for the
49098
+ * overwhelmingly common case — a policy with no unique-id principal makes NO
49099
+ * AWS call at all.
49100
+ *
49101
+ * Both sides are rewritten to the ARN form, never one side only. The race that
49102
+ * produces the mismatch is symmetric (the 2026-08-10 evidence on the
49103
+ * `drift-revert` fixture had the baseline holding `AROA…` and AWS the ARN, but
49104
+ * the reverse ordering is equally reachable), and normalizing a single side is
49105
+ * the mistake `drift-normalize.ts`'s header already records for the unordered
49106
+ * passes — it manufactures drift on the `properties`-fallback baseline.
49107
+ *
49108
+ * FAILING IS VISIBLE, never silent: when the resolver cannot answer (the role
49109
+ * was deleted — which is exactly when AWS keeps the unique id forever — the
49110
+ * caller lacks `iam:GetRole`, or the ARN names another ACCOUNT / IAM PATH, for
49111
+ * which the name-only `GetRole` API would otherwise answer about the caller's
49112
+ * own same-named role), both sides are left untouched and the drift is still
49113
+ * reported. This pass only ever removes a difference it has PROVEN to be two
49114
+ * spellings of one principal; it can never hide a real principal change.
49115
+ *
49116
+ * One downstream note: because the comparator's inputs are canonicalized,
49117
+ * `cdkd drift --accept` records the ARN form for a policy subtree that drifts
49118
+ * for some OTHER reason while also carrying a canonicalized principal — i.e.
49119
+ * the accepted baseline can hold a spelling AWS did not literally return. That
49120
+ * is benign (it is the form AWS re-canonicalizes to, and a later run
49121
+ * normalizes either way) but it is a real difference from "state records
49122
+ * exactly what AWS said", so it is written down rather than left to be
49123
+ * rediscovered.
49124
+ *
49125
+ * Known bound: a policy carried as a JSON STRING rather than a parsed object is
49126
+ * not walked. Re-serializing a parsed string would risk changing its formatting
49127
+ * and manufacturing a different phantom drift, so the pass is scoped to the
49128
+ * object shape — which is what `AWS::S3::BucketPolicy` and the other
49129
+ * `PolicyDocument`-typed properties carry in cdkd state.
49130
+ */
49131
+ /**
49132
+ * An IAM unique id as it appears in a rendered policy. The prefix set is
49133
+ * deliberately narrow — `AROA` (role) and `AIDA` (user) are the only two that
49134
+ * can stand in for an ARN at a `Principal.AWS` position; `AGPA` (group),
49135
+ * `AKIA` (access key) and friends are never rendered there.
49136
+ */
49137
+ const PRINCIPAL_UNIQUE_ID_RE = /^A(?:ROA|IDA)[A-Z0-9]{6,}$/;
49138
+ /** An IAM role / user ARN — the two shapes a unique id can stand in for. */
49139
+ const IAM_PRINCIPAL_ARN_RE = /^arn:[a-z0-9-]+:iam::\d{12}:(role|user)\/(.+)$/;
49140
+ /**
49141
+ * Split an IAM role / user principal ARN into what the lookup API needs, or
49142
+ * `undefined` when the string is not one (a service principal, an
49143
+ * `assumed-role` STS ARN, `*`, …).
49144
+ *
49145
+ * Exported so the resolver in `drift.ts` recognizes exactly the ARNs
49146
+ * {@link collectPrincipalArns} collects — one regex, so the collector and the
49147
+ * lookup cannot drift apart and leave an ARN gathered but never resolvable.
49148
+ */
49149
+ function parseIamPrincipalArn(arn) {
49150
+ const match = IAM_PRINCIPAL_ARN_RE.exec(arn);
49151
+ if (!match) return void 0;
49152
+ const name = match[2].split("/").pop();
49153
+ if (!name) return void 0;
49154
+ return {
49155
+ kind: match[1],
49156
+ name
49157
+ };
49158
+ }
49159
+ /** Every string at a `Principal` / `NotPrincipal` position, at any depth. */
49160
+ function collectPrincipalStrings(value, out) {
49161
+ if (Array.isArray(value)) {
49162
+ for (const element of value) collectPrincipalStrings(element, out);
49163
+ return;
49164
+ }
49165
+ if (!value || typeof value !== "object") return;
49166
+ for (const [key, member] of Object.entries(value)) {
49167
+ if (key === "Principal" || key === "NotPrincipal") pushPrincipalValue(member, out);
49168
+ collectPrincipalStrings(member, out);
49169
+ }
49170
+ }
49171
+ /**
49172
+ * The strings a `Principal` / `NotPrincipal` value contributes: the bare string
49173
+ * form, and the `AWS` member of the object form (string or array). `Service` /
49174
+ * `Federated` / `CanonicalUser` are deliberately NOT read — none of them can
49175
+ * carry an IAM unique id or a role ARN.
49176
+ */
49177
+ function pushPrincipalValue(value, out) {
49178
+ if (typeof value === "string") {
49179
+ out.push(value);
49180
+ return;
49181
+ }
49182
+ if (Array.isArray(value)) {
49183
+ for (const element of value) if (typeof element === "string") out.push(element);
49184
+ return;
49185
+ }
49186
+ if (!value || typeof value !== "object") return;
49187
+ const awsMember = value["AWS"];
49188
+ if (typeof awsMember === "string") out.push(awsMember);
49189
+ else if (Array.isArray(awsMember)) {
49190
+ for (const element of awsMember) if (typeof element === "string") out.push(element);
49191
+ }
49192
+ }
49193
+ /**
49194
+ * Both interesting principal kinds in ONE walk — the pass runs per resource on
49195
+ * both comparison sides, so walking the bag twice to answer two questions about
49196
+ * the same strings is pure cost.
49197
+ */
49198
+ function collectPrincipalForms(value) {
49199
+ const strings = [];
49200
+ collectPrincipalStrings(value, strings);
49201
+ const uniqueIds = /* @__PURE__ */ new Set();
49202
+ const arns = /* @__PURE__ */ new Set();
49203
+ for (const s of strings) if (PRINCIPAL_UNIQUE_ID_RE.test(s)) uniqueIds.add(s);
49204
+ else if (IAM_PRINCIPAL_ARN_RE.test(s)) arns.add(s);
49205
+ return {
49206
+ uniqueIds,
49207
+ arns
49208
+ };
49209
+ }
49210
+ /**
49211
+ * Replace every principal string that is a KNOWN unique id with the ARN it was
49212
+ * proven to stand for. Only `Principal` / `NotPrincipal` positions are touched
49213
+ * — a unique id appearing in, say, a `Condition` value is left alone, where the
49214
+ * two forms are NOT interchangeable.
49215
+ */
49216
+ function rewritePrincipalUniqueIds(value, arnByUniqueId) {
49217
+ if (Array.isArray(value)) return value.map((el) => rewritePrincipalUniqueIds(el, arnByUniqueId));
49218
+ if (!value || typeof value !== "object") return value;
49219
+ const out = {};
49220
+ for (const [key, member] of Object.entries(value)) out[key] = key === "Principal" || key === "NotPrincipal" ? rewritePrincipalValue(member, arnByUniqueId) : rewritePrincipalUniqueIds(member, arnByUniqueId);
49221
+ return out;
49222
+ }
49223
+ function rewritePrincipalValue(value, arnByUniqueId) {
49224
+ const swap = (s) => typeof s === "string" ? arnByUniqueId.get(s) ?? s : rewritePrincipalUniqueIds(s, arnByUniqueId);
49225
+ if (typeof value === "string") return swap(value);
49226
+ if (Array.isArray(value)) return value.map(swap);
49227
+ if (!value || typeof value !== "object") return value;
49228
+ const out = {};
49229
+ for (const [key, member] of Object.entries(value)) {
49230
+ if (key !== "AWS") {
49231
+ out[key] = rewritePrincipalUniqueIds(member, arnByUniqueId);
49232
+ continue;
49233
+ }
49234
+ out[key] = Array.isArray(member) ? member.map(swap) : swap(member);
49235
+ }
49236
+ return out;
49237
+ }
49238
+ /**
49239
+ * Canonicalize the two comparison sides so that a principal AWS rendered as a
49240
+ * unique id on one side and as an ARN on the other stops reading as drift.
49241
+ *
49242
+ * Short-circuits — with NO resolver call — unless BOTH a unique id and an IAM
49243
+ * ARN are present across the two sides, since a unique id with no candidate ARN
49244
+ * opposite it has nothing to be equal to.
49245
+ */
49246
+ async function canonicalizePrincipalUniqueIds(baseline, aws, resolve) {
49247
+ const baselineForms = collectPrincipalForms(baseline);
49248
+ const awsForms = collectPrincipalForms(aws);
49249
+ const uniqueIds = /* @__PURE__ */ new Set([...baselineForms.uniqueIds, ...awsForms.uniqueIds]);
49250
+ if (uniqueIds.size === 0) return {
49251
+ baseline,
49252
+ aws
49253
+ };
49254
+ const arns = /* @__PURE__ */ new Set([...baselineForms.arns, ...awsForms.arns]);
49255
+ if (arns.size === 0) return {
49256
+ baseline,
49257
+ aws
49258
+ };
49259
+ const arnByUniqueId = /* @__PURE__ */ new Map();
49260
+ for (const arn of arns) {
49261
+ const uniqueId = await resolve(arn);
49262
+ if (uniqueId !== void 0 && uniqueIds.has(uniqueId)) arnByUniqueId.set(uniqueId, arn);
49263
+ }
49264
+ if (arnByUniqueId.size === 0) return {
49265
+ baseline,
49266
+ aws
49267
+ };
49268
+ return {
49269
+ baseline: rewritePrincipalUniqueIds(baseline, arnByUniqueId),
49270
+ aws: rewritePrincipalUniqueIds(aws, arnByUniqueId)
49271
+ };
49272
+ }
49273
+
48881
49274
  //#endregion
48882
49275
  //#region src/analyzer/drift-cc-api-deny-list.ts
48883
49276
  /**
@@ -49092,11 +49485,12 @@ async function driftCommand(stacks, options) {
49092
49485
  registerAllProviders(providerRegistry);
49093
49486
  providerRegistry.setCustomResourceResponseBucket(bucket);
49094
49487
  const ccApiFallback = new CloudControlProvider();
49488
+ const resolvePrincipalUniqueId = createIamPrincipalUniqueIdResolver(awsClients);
49095
49489
  const targetRefs = resolveTargetRefs(stacks, await stateBackend.listStacks(), options);
49096
49490
  const reports = [];
49097
49491
  for (const ref of targetRefs) {
49098
49492
  if (!ref.region) throw new Error(`Stack '${ref.stackName}' has only a legacy state record without a region. Run 'cdkd deploy ${ref.stackName}' (or any cdkd write) to migrate it to the region-scoped layout, then re-run drift detection.`);
49099
- const report = await runDriftForStack(ref.stackName, ref.region, stateBackend, providerRegistry, ccApiFallback);
49493
+ const report = await runDriftForStack(ref.stackName, ref.region, stateBackend, providerRegistry, ccApiFallback, resolvePrincipalUniqueId);
49100
49494
  reports.push(report);
49101
49495
  }
49102
49496
  if (options.json) writeJsonReport(reports);
@@ -49158,13 +49552,75 @@ function resolveTargetRefs(stacks, stateRefs, options) {
49158
49552
  return out;
49159
49553
  }
49160
49554
  /**
49555
+ * The live half of the #1515 principal canonicalization: resolve an IAM role /
49556
+ * user ARN to that principal's unique id (`AROA…` / `AIDA…`).
49557
+ *
49558
+ * Results are cached per ARN for the whole command — INCLUDING the failures, so
49559
+ * a deleted role (the case where AWS keeps the unique id forever, and therefore
49560
+ * the likeliest one to appear here) costs exactly one API call rather than one
49561
+ * per resource that references it.
49562
+ *
49563
+ * Every failure resolves to `undefined` rather than throwing: a missing
49564
+ * `iam:GetRole` permission, a cross-account principal, or a deleted role must
49565
+ * leave the comparison exactly as it was — the drift is then reported, which is
49566
+ * the safe direction. Drift detection must never fail because a cosmetic
49567
+ * normalization could not run.
49568
+ *
49569
+ * **The response ARN is compared back to the requested one, and that check is
49570
+ * what makes the "never hides a real change" claim true rather than aspirational.**
49571
+ * `GetRole` / `GetUser` are account-local and take a NAME, not an ARN, so the
49572
+ * lookup silently answers about the CALLER's account and about the wrong IAM
49573
+ * path: `arn:aws:iam::<other-acct>:role/Foo` and
49574
+ * `arn:aws:iam::<self>:role/prod/Foo` would both come back as this account's
49575
+ * root-path `Foo`, and the pass would then "prove" two DIFFERENT principals
49576
+ * equal and collapse a genuine drift. Round-tripping the ARN costs nothing and
49577
+ * turns both cases into an unresolved lookup, i.e. drift reported.
49578
+ */
49579
+ function createIamPrincipalUniqueIdResolver(awsClients) {
49580
+ const cache = /* @__PURE__ */ new Map();
49581
+ let warnedOnDenied = false;
49582
+ return async (arn) => {
49583
+ if (cache.has(arn)) return cache.get(arn);
49584
+ let uniqueId;
49585
+ const principal = parseIamPrincipalArn(arn);
49586
+ if (principal) try {
49587
+ let entityArn;
49588
+ let entityId;
49589
+ if (principal.kind === "role") {
49590
+ const role = (await awsClients.iam.send(new GetRoleCommand({ RoleName: principal.name }))).Role;
49591
+ entityArn = role?.Arn;
49592
+ entityId = role?.RoleId;
49593
+ } else {
49594
+ const user = (await awsClients.iam.send(new GetUserCommand({ UserName: principal.name }))).User;
49595
+ entityArn = user?.Arn;
49596
+ entityId = user?.UserId;
49597
+ }
49598
+ if (entityArn !== void 0 && entityArn.toLowerCase() === arn.toLowerCase()) uniqueId = entityId;
49599
+ else getLogger().debug(`Principal ${arn} resolved to ${entityArn ?? "no entity"} — same name in this account or under another IAM path; leaving the policy principal comparison untouched.`);
49600
+ } catch (error) {
49601
+ const message = error instanceof Error ? error.message : String(error);
49602
+ const name = error instanceof Error ? error.name : "";
49603
+ const status = error?.$metadata?.httpStatusCode;
49604
+ const denied = /^AccessDenied|^NotAuthorized/i.test(name) || status === 403;
49605
+ if (!warnedOnDenied && denied) {
49606
+ warnedOnDenied = true;
49607
+ getLogger().warn(`Cannot read IAM principals (${name || "access denied"}). A resource policy whose principal AWS rendered as a unique id (AROA…/AIDA…) may therefore report drift that is only a spelling difference; grant iam:GetRole / iam:GetUser to resolve it, or re-run with --verbose for the full error.`);
49608
+ }
49609
+ getLogger().debug(`Could not resolve the unique id of principal ${arn} (${message}); leaving the policy principal comparison untouched.`);
49610
+ if (isThrottlingError(error)) return void 0;
49611
+ }
49612
+ cache.set(arn, uniqueId);
49613
+ return uniqueId;
49614
+ };
49615
+ }
49616
+ /**
49161
49617
  * Run drift detection for one stack and shape the per-resource outcomes
49162
49618
  * into a {@link StackDriftReport}. The state object + etag are stored on
49163
49619
  * the report so `--accept` can write back without a re-read, and
49164
49620
  * `--revert` can pass the captured AWS-current snapshot to
49165
49621
  * `provider.update` as the `previousProperties` argument.
49166
49622
  */
49167
- async function runDriftForStack(stackName, region, stateBackend, providerRegistry, ccApiFallback) {
49623
+ async function runDriftForStack(stackName, region, stateBackend, providerRegistry, ccApiFallback, resolvePrincipalUniqueId) {
49168
49624
  const result = await stateBackend.getState(stackName, region);
49169
49625
  if (!result) throw new Error(`No state found for stack '${stackName}' (${region}). Run 'cdkd state list' to see available stacks.`);
49170
49626
  return await withStackName(stackName, async () => {
@@ -49230,7 +49686,8 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
49230
49686
  const useObserved = resource.observedProperties !== void 0;
49231
49687
  const baseline = useObserved ? resource.observedProperties : resource.properties ?? {};
49232
49688
  const observedIgnorePaths = useObserved ? undeclaredEmptyObservedKeys(resource.observedProperties, resource.properties ?? {}) : [];
49233
- const changes = calculateResourceDrift(baseline, aws, {
49689
+ const normalized = await canonicalizePrincipalUniqueIds(baseline, aws, resolvePrincipalUniqueId);
49690
+ const changes = calculateResourceDrift(normalized.baseline, normalized.aws, {
49234
49691
  ignorePaths: observedIgnorePaths.length ? [...ignorePaths, ...observedIgnorePaths] : ignorePaths,
49235
49692
  unionWalkObjects: useObserved,
49236
49693
  unorderedPaths
@@ -66706,7 +67163,7 @@ function createMigrateCommand() {
66706
67163
  */
66707
67164
  function buildProgram() {
66708
67165
  const program = new Command();
66709
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.36");
67166
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.38");
66710
67167
  program.hook("preAction", (_thisCommand, actionCommand) => {
66711
67168
  const { profile } = actionCommand.optsWithGlobals();
66712
67169
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;