@go-to-k/cdkd 0.282.7 → 0.283.1

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 configStringRefusal, $t as resolveStateBucketWithDefaultAndSource, A as green, An as NestedStackChildDirectDestroyError, At as BOOTSTRAP_MARKER_PREFIX, B as slowCcOperationTimeoutMs, Bt as runDockerForeground, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Ct as stringifyValue, D as bold, Dn as LocalStartServiceError, Dt as loadPublishableAssetManifest, E as formatResourceLine, En as LocalMigrateError, Et as createAssetRedirectResolver, F as clearOnUpdateRemoval, Fn as StackHasActiveImportsError, Ft as validateContainerRepoName, G as getAccountInfo, Gt as synthesisStatusMessage, H as isTerminationProtectionPropagationError, Hn as withErrorHandling, Ht as AssetManifestLoader, I as ProviderRegistry, In as StackTerminationProtectionError, It as buildDenyExternalAccessPolicy, J as normalizeAwsTagsToCfn, Jt as resolveApp, K as refStateLookupFromResource, Kt as getDefaultStateBucketName, L as findActionableSilentDrops, Lt as buildDockerImage, M as yellow, Mn as ProvisioningError, Mt as getBootstrapMarkerKey, N as IAMRoleProvider, Nn as ResourceTimeoutError, Nt as parseBootstrapMarker, O as cyan, Ot as rewriteTemplateAssetReferences, P as collectInlinePolicyNamesManagedBySiblings, Pn as ResourceUpdateNotSupportedError, Pt as validateAssetBucketName, Q as configBooleanRefusal, Qt as resolveStateBucketWithDefault, R as findSilentDropProperties, Rt as formatDockerLoginError, S as extractDeploymentEventError, St as AssetPublisher, T as renderStatefulReason, Tn as LocalInvokeBuildError$1, Tt as buildAssetRedirectMap, U as IntrinsicFunctionResolver, Ut as getDockerImageBySourceHash, V as disableInstanceApiTermination, Vn as normalizeAwsError, Vt as runDockerStreaming, W as cfnRefValueFromPhysicalId, Wt as Synthesizer, X as assertRegionMatch, Xt as resolveCaptureObservedState, Y as resolveExplicitPhysicalId, Yt as resolveAutoAssetStorage, Z as coerceCfnBoolean, Zt as resolveSkipPrefix, _ as createPreDeleteFinalSnapshot, _n as getAwsClients, _t as TemplateParser, a as DeploymentEventsStore, an as MIGRATE_TMP_PREFIX, at as s3BucketArn, b as unsupportedFinalSnapshotError, bt as rebuildClientForBucketRegion, c as replayFailedOperations, cn as expectedOwnerParam, ct as s3BucketRegionalDomainName, d as IMPLICIT_DELETE_DEPENDENCIES, dn as derivePartitionAndUrlSuffix, dt as DiffCalculator, en as resolveUseCdkBootstrapAssets, et as readConfigString, f as computeImplicitDeleteEdges, fn as AssemblyReader, ft as describeTypeWithThrottleRetry, g as ccRoutedFinalSnapshotError, gn as AwsClients, gt as DagBuilder, h as buildFinalSnapshotIdentifier, hn as resolveBucketRegion, ht as isThrottlingError, i as DeploymentEventsReader, in as CFN_TEMPLATE_URL_LIMIT, it as requireConfigString, j as red, jn as PartialFailureError, jt as ensureAssetStorage, k as gray, kn as MissingCdkCliError, kt as AssetModeResolver, l as replayRollback, ln as PARTITION_TABLE, lt as s3BucketWebsiteUrl, m as PRE_DELETE_SNAPSHOT_TYPES, mt as isRetryableTransientError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as warnDeprecatedNoPrefixCliFlag, nt as requireConfigArray, o as planFailedOps, on as findLargeInlineResources, ot as s3BucketDomainName, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as processStackMessages, pt as withRetry, q as WAFv2WebACLProvider, qt as getLegacyStateBucketName, r as DeployEngine, rn as CFN_TEMPLATE_BODY_LIMIT, rt as requireConfigObject, s as planRollback, sn as uploadCfnTemplate, st as s3BucketDualStackDomainName, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as stateBucketExistenceConfirmed, tt as replayWarn, u as withResourceDeadline, un as canonicalizeRegion, ut as applyRoleArnIfSet, v as isFinalSnapshotError, vn as resetAwsClients, vt as LockManager, w as isStatefulRecreateTargetSync, wn as DeployCancelledError, wt as WorkGraph, x as makeCanonicalizePropertiesFn, xn as CdkdError, xt as shouldRetainResource, y as refusesFinalSnapshot, yn as setAwsClients, yt as S3StateBackend, z as CloudControlProvider, zt as getDockerCmd } from "./deploy-engine-NA36xDvk.js";
2
+ import { $ as configStringRefusal, $t as resolveStateBucketWithDefaultAndSource, A as green, An as NestedStackChildDirectDestroyError, At as BOOTSTRAP_MARKER_PREFIX, B as slowCcOperationTimeoutMs, Bt as runDockerForeground, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Ct as stringifyValue, D as bold, Dn as LocalStartServiceError, Dt as loadPublishableAssetManifest, E as formatResourceLine, En as LocalMigrateError, Et as createAssetRedirectResolver, F as clearOnUpdateRemoval, Fn as StackHasActiveImportsError, Ft as validateContainerRepoName, G as getAccountInfo, Gt as synthesisStatusMessage, H as isTerminationProtectionPropagationError, Hn as withErrorHandling, Ht as AssetManifestLoader, I as ProviderRegistry, In as StackTerminationProtectionError, It as buildDenyExternalAccessPolicy, J as normalizeAwsTagsToCfn, Jt as resolveApp, K as refStateLookupFromResource, Kt as getDefaultStateBucketName, L as findActionableSilentDrops, Lt as buildDockerImage, M as yellow, Mn as ProvisioningError, Mt as getBootstrapMarkerKey, N as IAMRoleProvider, Nn as ResourceTimeoutError, Nt as parseBootstrapMarker, O as cyan, Ot as rewriteTemplateAssetReferences, P as collectInlinePolicyNamesManagedBySiblings, Pn as ResourceUpdateNotSupportedError, Pt as validateAssetBucketName, Q as configBooleanRefusal, Qt as resolveStateBucketWithDefault, R as findSilentDropProperties, Rt as formatDockerLoginError, S as extractDeploymentEventError, St as AssetPublisher, T as renderStatefulReason, Tn as LocalInvokeBuildError$1, Tt as buildAssetRedirectMap, U as IntrinsicFunctionResolver, Ut as getDockerImageBySourceHash, V as disableInstanceApiTermination, Vn as normalizeAwsError, Vt as runDockerStreaming, W as cfnRefValueFromPhysicalId, Wt as Synthesizer, X as assertRegionMatch, Xt as resolveCaptureObservedState, Y as resolveExplicitPhysicalId, Yt as resolveAutoAssetStorage, Z as coerceCfnBoolean, Zt as resolveSkipPrefix, _ as createPreDeleteFinalSnapshot, _n as getAwsClients, _t as TemplateParser, a as DeploymentEventsStore, an as MIGRATE_TMP_PREFIX, at as s3BucketArn, b as unsupportedFinalSnapshotError, bt as rebuildClientForBucketRegion, c as replayFailedOperations, cn as expectedOwnerParam, ct as s3BucketRegionalDomainName, d as IMPLICIT_DELETE_DEPENDENCIES, dn as derivePartitionAndUrlSuffix, dt as DiffCalculator, en as resolveUseCdkBootstrapAssets, et as readConfigString, f as computeImplicitDeleteEdges, fn as AssemblyReader, ft as describeTypeWithThrottleRetry, g as ccRoutedFinalSnapshotError, gn as AwsClients, gt as DagBuilder, h as buildFinalSnapshotIdentifier, hn as resolveBucketRegion, ht as isThrottlingError, i as DeploymentEventsReader, in as CFN_TEMPLATE_URL_LIMIT, it as requireConfigString, j as red, jn as PartialFailureError, jt as ensureAssetStorage, k as gray, kn as MissingCdkCliError, kt as AssetModeResolver, l as replayRollback, ln as PARTITION_TABLE, lt as s3BucketWebsiteUrl, m as PRE_DELETE_SNAPSHOT_TYPES, mt as isRetryableTransientError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as warnDeprecatedNoPrefixCliFlag, nt as requireConfigArray, o as planFailedOps, on as findLargeInlineResources, ot as s3BucketDomainName, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as processStackMessages, pt as withRetry, q as WAFv2WebACLProvider, qt as getLegacyStateBucketName, r as DeployEngine, rn as CFN_TEMPLATE_BODY_LIMIT, rt as requireConfigObject, s as planRollback, sn as uploadCfnTemplate, st as s3BucketDualStackDomainName, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as stateBucketExistenceConfirmed, tt as replayWarn, u as withResourceDeadline, un as canonicalizeRegion, ut as applyRoleArnIfSet, v as isFinalSnapshotError, vn as resetAwsClients, vt as LockManager, w as isStatefulRecreateTargetSync, wn as DeployCancelledError, wt as WorkGraph, x as makeCanonicalizePropertiesFn, xn as CdkdError, xt as shouldRetainResource, y as refusesFinalSnapshot, yn as setAwsClients, yt as S3StateBackend, z as CloudControlProvider, zt as getDockerCmd } from "./deploy-engine-InwDI9o0.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
4
- import { t as ASGProvider } from "./asg-provider-DulIbRCK.js";
4
+ import { t as ASGProvider } from "./asg-provider-R3IdAR_3.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";
@@ -10,7 +10,7 @@ import { CreateQueueCommand, DeleteQueueCommand, GetQueueAttributesCommand, GetQ
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, DeleteFunctionCodeSigningConfigCommand, DeleteFunctionCommand, DeleteFunctionConcurrencyCommand, DeleteFunctionEventInvokeConfigCommand, DeleteFunctionUrlConfigCommand, DeleteLayerVersionCommand, GetEventSourceMappingCommand, GetFunctionCodeSigningConfigCommand, GetFunctionCommand, GetFunctionConcurrencyCommand, GetFunctionEventInvokeConfigCommand, GetFunctionRecursionConfigCommand, GetFunctionUrlConfigCommand, GetLayerVersionByArnCommand, GetPolicyCommand as GetPolicyCommand$1, GetRuntimeManagementConfigCommand, LambdaClient, ListTagsCommand, PublishLayerVersionCommand, PutFunctionCodeSigningConfigCommand, PutFunctionConcurrencyCommand, PutFunctionEventInvokeConfigCommand, PutFunctionRecursionConfigCommand, PutRuntimeManagementConfigCommand, RemovePermissionCommand, ResourceNotFoundException, TagResourceCommand as TagResourceCommand$1, UntagResourceCommand as UntagResourceCommand$1, UpdateEventSourceMappingCommand, UpdateFunctionCodeCommand, UpdateFunctionConfigurationCommand, UpdateFunctionUrlConfigCommand, waitUntilFunctionActiveV2, waitUntilFunctionUpdatedV2 } from "@aws-sdk/client-lambda";
12
12
  import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
13
- import { AllocateAddressCommand, AssociateAddressCommand, AssociateIamInstanceProfileCommand, AssociateRouteTableCommand, AttachInternetGatewayCommand, AuthorizeSecurityGroupEgressCommand, AuthorizeSecurityGroupIngressCommand, CreateInternetGatewayCommand, CreateNatGatewayCommand, CreateNetworkAclCommand, CreateNetworkAclEntryCommand, CreateRouteCommand, CreateRouteTableCommand, CreateSecurityGroupCommand, CreateSubnetCommand, CreateTagsCommand, CreateVpcCommand, DeleteInternetGatewayCommand, DeleteNatGatewayCommand, DeleteNetworkAclCommand, DeleteNetworkAclEntryCommand, DeleteNetworkInterfaceCommand, DeleteRouteCommand, DeleteRouteTableCommand, DeleteSecurityGroupCommand, DeleteSubnetCommand, DeleteTagsCommand, DeleteVpcCommand, DescribeAddressesCommand, DescribeAvailabilityZonesCommand, DescribeIamInstanceProfileAssociationsCommand, DescribeInstanceAttributeCommand, DescribeInstanceCreditSpecificationsCommand, DescribeInstancesCommand, DescribeInternetGatewaysCommand, DescribeNatGatewaysCommand, DescribeNetworkAclsCommand, DescribeNetworkInterfacesCommand, DescribeRouteTablesCommand, DescribeSecurityGroupsCommand, DescribeSubnetsCommand, DescribeVolumesCommand, DescribeVpcAttributeCommand, DescribeVpcsCommand, DetachInternetGatewayCommand, DisassociateAddressCommand, DisassociateRouteTableCommand, EC2Client, ModifyInstanceAttributeCommand, ModifyInstanceCreditSpecificationCommand, ModifyInstanceMetadataOptionsCommand, ModifySubnetAttributeCommand, ModifyVpcAttributeCommand, MonitorInstancesCommand, ReleaseAddressCommand, ReplaceNetworkAclAssociationCommand, RevokeSecurityGroupEgressCommand, RevokeSecurityGroupIngressCommand, RunInstancesCommand, TerminateInstancesCommand, UnmonitorInstancesCommand, waitUntilInstanceRunning, waitUntilInstanceTerminated, waitUntilNatGatewayAvailable, waitUntilNatGatewayDeleted } from "@aws-sdk/client-ec2";
13
+ import { AllocateAddressCommand, AssociateAddressCommand, AssociateIamInstanceProfileCommand, AssociateRouteTableCommand, AttachInternetGatewayCommand, AuthorizeSecurityGroupEgressCommand, AuthorizeSecurityGroupIngressCommand, CreateInternetGatewayCommand, CreateNatGatewayCommand, CreateNetworkAclCommand, CreateNetworkAclEntryCommand, CreateRouteCommand, CreateRouteTableCommand, CreateSecurityGroupCommand, CreateSubnetCommand, CreateTagsCommand, CreateVpcCommand, DeleteInternetGatewayCommand, DeleteNatGatewayCommand, DeleteNetworkAclCommand, DeleteNetworkAclEntryCommand, DeleteNetworkInterfaceCommand, DeleteRouteCommand, DeleteRouteTableCommand, DeleteSecurityGroupCommand, DeleteSubnetCommand, DeleteTagsCommand, DeleteVpcCommand, DescribeAddressesCommand, DescribeAvailabilityZonesCommand, DescribeIamInstanceProfileAssociationsCommand, DescribeInstanceAttributeCommand, DescribeInstanceCreditSpecificationsCommand, DescribeInstancesCommand, DescribeInternetGatewaysCommand, DescribeNatGatewaysCommand, DescribeNetworkAclsCommand, DescribeNetworkInterfacesCommand, DescribeRouteTablesCommand, DescribeSecurityGroupRulesCommand, DescribeSecurityGroupsCommand, DescribeSubnetsCommand, DescribeVolumesCommand, DescribeVpcAttributeCommand, DescribeVpcsCommand, DetachInternetGatewayCommand, DisassociateAddressCommand, DisassociateRouteTableCommand, EC2Client, ModifyInstanceAttributeCommand, ModifyInstanceCreditSpecificationCommand, ModifyInstanceMetadataOptionsCommand, ModifySubnetAttributeCommand, ModifyVpcAttributeCommand, MonitorInstancesCommand, ReleaseAddressCommand, ReplaceNetworkAclAssociationCommand, RevokeSecurityGroupEgressCommand, RevokeSecurityGroupIngressCommand, RunInstancesCommand, TerminateInstancesCommand, UnmonitorInstancesCommand, waitUntilInstanceRunning, waitUntilInstanceTerminated, waitUntilNatGatewayAvailable, waitUntilNatGatewayDeleted } from "@aws-sdk/client-ec2";
14
14
  import { CreateTableCommand, DeleteResourcePolicyCommand, DeleteTableCommand, DescribeContinuousBackupsCommand, DescribeContributorInsightsCommand, DescribeKinesisStreamingDestinationCommand, DescribeTableCommand, DescribeTimeToLiveCommand, DisableKinesisStreamingDestinationCommand, DynamoDBClient, EnableKinesisStreamingDestinationCommand, GetResourcePolicyCommand, ListTagsOfResourceCommand, PutResourcePolicyCommand, ResourceNotFoundException as ResourceNotFoundException$1, TagResourceCommand as TagResourceCommand$2, UntagResourceCommand as UntagResourceCommand$2, UpdateContinuousBackupsCommand, UpdateContributorInsightsCommand, UpdateTableCommand, UpdateTimeToLiveCommand } from "@aws-sdk/client-dynamodb";
15
15
  import { CloudFormationClient, CreateChangeSetCommand, DeleteChangeSetCommand, DeleteStackCommand, DescribeChangeSetCommand, DescribeStackEventsCommand, DescribeStackResourcesCommand, DescribeStacksCommand, ExecuteChangeSetCommand, GetTemplateCommand, UpdateStackCommand, waitUntilChangeSetCreateComplete, waitUntilStackDeleteComplete, waitUntilStackImportComplete, waitUntilStackUpdateComplete } from "@aws-sdk/client-cloudformation";
16
16
  import { APIGatewayClient, CreateAuthorizerCommand, CreateDeploymentCommand, CreateResourceCommand, CreateStageCommand, DeleteAuthorizerCommand, DeleteDeploymentCommand, DeleteMethodCommand, DeleteResourceCommand, DeleteStageCommand, GetAccountCommand, GetAuthorizerCommand, GetDeploymentCommand, GetMethodCommand, GetResourceCommand, GetStageCommand, NotFoundException as NotFoundException$1, PutIntegrationCommand, PutIntegrationResponseCommand, PutMethodCommand, PutMethodResponseCommand, TagResourceCommand as TagResourceCommand$3, UntagResourceCommand as UntagResourceCommand$3, UpdateAccountCommand, UpdateAuthorizerCommand, UpdateMethodCommand, UpdateStageCommand } from "@aws-sdk/client-api-gateway";
@@ -15484,10 +15484,16 @@ var DynamoDBGlobalTableProvider = class {
15484
15484
  };
15485
15485
  if (tableArn) await this.applyTagDiff(tableArn, extractLocalTags(previousProperties), extractLocalTags(properties));
15486
15486
  const recordedOldBilling = previousProperties["BillingMode"];
15487
- const recordedOldBillingUsable = recordedOldBilling === void 0 || typeof recordedOldBilling === "string" && recordedOldBilling.trim() !== "";
15488
- const liveBillingMode = describeResp.Table?.BillingModeSummary?.BillingMode ?? "PROVISIONED";
15489
- const oldBilling = recordedOldBillingUsable ? recordedOldBilling ?? "PAY_PER_REQUEST" : liveBillingMode;
15490
- if (!recordedOldBillingUsable) this.logger.warn(`AWS::DynamoDB::GlobalTable ${logicalId}: the recorded previous BillingMode is unusable (${JSON.stringify(recordedOldBilling)}) using the table's actual billing mode (${oldBilling}) as the comparison baseline for this update so a corrected template does not issue a same-mode UpdateTable.`);
15487
+ const recordedOldBillingAbsent = recordedOldBilling === void 0;
15488
+ const recordedOldBillingUsable = typeof recordedOldBilling === "string" && recordedOldBilling.trim() !== "";
15489
+ const rawLiveBillingMode = describeResp.Table?.BillingModeSummary?.BillingMode;
15490
+ const reportedBillingMode = rawLiveBillingMode === "PROVISIONED" || rawLiveBillingMode === "PAY_PER_REQUEST" ? rawLiveBillingMode : void 0;
15491
+ const liveBillingMode = reportedBillingMode ?? "PROVISIONED";
15492
+ const desiredDeclaresBillingMode = properties["BillingMode"] !== void 0;
15493
+ const seedAbsentFromLive = recordedOldBillingAbsent && desiredDeclaresBillingMode;
15494
+ const oldBilling = recordedOldBillingUsable ? recordedOldBilling : recordedOldBillingAbsent ? seedAbsentFromLive ? liveBillingMode : "PAY_PER_REQUEST" : liveBillingMode;
15495
+ if (!recordedOldBillingUsable && !recordedOldBillingAbsent) this.logger.warn(`AWS::DynamoDB::GlobalTable ${logicalId}: the recorded previous BillingMode is unusable (${JSON.stringify(recordedOldBilling)}) — using the table's actual billing mode (${oldBilling}) as the comparison baseline for this update so a corrected template does not issue a same-mode UpdateTable.`);
15496
+ if (seedAbsentFromLive && oldBilling !== "PAY_PER_REQUEST") this.logger.warn(`AWS::DynamoDB::GlobalTable ${logicalId}: the cdkd state record declares no BillingMode — using ${oldBilling} as the comparison baseline for this update (${reportedBillingMode !== void 0 ? "the mode DescribeTable reports" : "inferred: DescribeTable reports no BillingModeSummary, which means the table was created without an explicit mode"}), so a template declaring a different mode still applies the flip instead of comparing equal to the create-path default.`);
15491
15497
  let billingUnusable = false;
15492
15498
  const requestedBilling = requireConfigString(properties["BillingMode"], "PAY_PER_REQUEST", "AWS::DynamoDB::GlobalTable BillingMode", { onUnusable: (message) => {
15493
15499
  billingUnusable = true;
@@ -15575,6 +15581,7 @@ var DynamoDBGlobalTableProvider = class {
15575
15581
  effectiveProperties = { ...effectiveProperties ?? properties };
15576
15582
  if (recordedOldBilling === void 0) delete effectiveProperties["BillingMode"];
15577
15583
  else effectiveProperties["BillingMode"] = oldBilling;
15584
+ effectiveProperties = retainUnsendableCapacityMembers(effectiveProperties, previousProperties, newBilling);
15578
15585
  }
15579
15586
  const awsOnDemand = describeResp.Table?.OnDemandThroughput;
15580
15587
  const onDemandPayload = {};
@@ -17533,6 +17540,185 @@ function stripProvisionedCapacityKeys(properties) {
17533
17540
  return result;
17534
17541
  }
17535
17542
  /**
17543
+ * The capacity member keys that can only reach AWS while the table is
17544
+ * PROVISIONED, per container level (issue #1738).
17545
+ *
17546
+ * Identical to the set {@link stripProvisionedCapacityKeys} removes, and that
17547
+ * is not a coincidence: both answer "which members are unsendable once the mode
17548
+ * is PAY_PER_REQUEST". The two spellings a pre-#1387 cdkd wrote into state
17549
+ * (`ProvisionedThroughput` / `ProvisionedThroughputOverride`) are included for
17550
+ * the same reason — a state replay is exactly that population.
17551
+ */
17552
+ const PROVISIONED_ONLY_CAPACITY_KEYS = {
17553
+ table: ["WriteProvisionedThroughputSettings"],
17554
+ index: [
17555
+ "WriteProvisionedThroughputSettings",
17556
+ "ReadProvisionedThroughputSettings",
17557
+ "ProvisionedThroughput"
17558
+ ],
17559
+ replica: ["ReadProvisionedThroughputSettings", "ProvisionedThroughputOverride"],
17560
+ replicaIndex: ["ReadProvisionedThroughputSettings", "ProvisionedThroughputOverride"]
17561
+ };
17562
+ /**
17563
+ * The mirror set: capacity members that can only reach AWS while the table is
17564
+ * PAY_PER_REQUEST (issue #1738).
17565
+ *
17566
+ * `stripProvisionedCapacityKeys` has no equivalent because the create arm it
17567
+ * serves always substitutes PAY_PER_REQUEST — the mode under which these ARE
17568
+ * sent, which is why its header says they are deliberately not stripped. On the
17569
+ * UPDATE path the kept mode can be PROVISIONED, and then it is this half that
17570
+ * never leaves the process.
17571
+ */
17572
+ const ON_DEMAND_ONLY_CAPACITY_KEYS = {
17573
+ table: ["WriteOnDemandThroughputSettings"],
17574
+ index: [
17575
+ "WriteOnDemandThroughputSettings",
17576
+ "ReadOnDemandThroughputSettings",
17577
+ "OnDemandThroughput"
17578
+ ],
17579
+ replica: ["ReadOnDemandThroughputSettings", "OnDemandThroughputOverride"],
17580
+ replicaIndex: ["ReadOnDemandThroughputSettings", "OnDemandThroughputOverride"]
17581
+ };
17582
+ /**
17583
+ * Replace every capacity member the KEPT billing mode cannot send with the
17584
+ * PREVIOUS record's value (issue #1738).
17585
+ *
17586
+ * Called from `update()`'s `BillingMode` warn-and-KEEP arm, where an unusable
17587
+ * desired mode suppresses the flip: AWS is left holding the mode it already
17588
+ * had, so every block belonging to the OTHER mode was silently dropped on the
17589
+ * way out while the engine recorded what the template declared.
17590
+ *
17591
+ * **The split is per MEMBER against the KEPT mode**, and each side was read off
17592
+ * the gates in this file rather than off the member's name-shape (the
17593
+ * name-shape trap #1726 records — an on-demand ceiling looks like capacity and
17594
+ * goes on the wire under precisely the on-demand mode):
17595
+ *
17596
+ * - KEPT `PROVISIONED` — the on-demand half is unsendable. Step 3's table-level
17597
+ * `OnDemandThroughput` is gated on `newBilling !== 'PROVISIONED'`;
17598
+ * `toSdkGlobalSecondaryIndexes` and `toSdkReplicaThroughputOverrides` both
17599
+ * drop their on-demand branch under PROVISIONED. The provisioned half is NOT
17600
+ * retained: step 4b and step 6b's auto-scaling reconcile are live whenever
17601
+ * either side is PROVISIONED, and step 6's GSI diff carries per-index
17602
+ * `ProvisionedThroughput` — those blocks really are applied.
17603
+ * - KEPT `PAY_PER_REQUEST` — the provisioned half is unsendable. The table-level
17604
+ * `ProvisionedThroughput` rides step 4's flip and nothing else, the two
17605
+ * auto-scaling reconciles are gated on either side being PROVISIONED, and both
17606
+ * translators drop their provisioned branch. The on-demand half is NOT
17607
+ * retained: step 3 sends the table-level ceilings and step 6 the per-index
17608
+ * ones under exactly this mode.
17609
+ *
17610
+ * One residual is deliberately NOT covered, because the suppression did not
17611
+ * create it: the TABLE-level provisioned capacity NUMBER rides step 4's flip
17612
+ * and nothing else, so on ANY non-flip PROVISIONED -> PROVISIONED update it is
17613
+ * unsent while its `*CapacityAutoScalingSettings` sibling in the same block IS
17614
+ * reconciled. Retaining the block wholesale under a kept PROVISIONED mode would
17615
+ * therefore erase a real auto-scaling update to hide a pre-existing gap — so
17616
+ * the block stays as declared and the gap stays where it already lives.
17617
+ *
17618
+ * Retaining rather than DROPPING is the UPDATE row of
17619
+ * `.claude/rules/providers.md`: the call never ran, so AWS still holds the
17620
+ * previously-applied block and that IS what state should describe. Dropping
17621
+ * would leave a later template that REMOVES the block deriving no removal, so
17622
+ * the live setting would survive forever.
17623
+ *
17624
+ * **The previous side is VALIDATED before it is retained** (the #1653-review
17625
+ * rule) through `asRecord` — the SAME predicate every wire read of these blocks
17626
+ * applies, so the two cannot disagree about what a bare string / an array / a
17627
+ * number means. Be honest about what that buys: it refuses exactly the
17628
+ * not-a-plain-object shapes, and a block whose NUMERIC member is an unresolved
17629
+ * intrinsic passes it, because the wire accepts that block too (and then sends
17630
+ * nothing for the member, which is the same "AWS keeps what it had" outcome).
17631
+ * When the previous side is unusable OR absent the key is DROPPED — there is no
17632
+ * value cdkd can vouch for either way.
17633
+ *
17634
+ * COPIES each retained block one level rather than aliasing it: the rollback
17635
+ * executor spreads the returned bag shallowly, so handing back the previous
17636
+ * record's own object would leave two state records sharing one mutable value.
17637
+ * Scope that claim honestly — it covers the retained blocks only; every other
17638
+ * key of the input bag stays aliased, exactly as the sibling arms leave it.
17639
+ *
17640
+ * Deliberately NOT paired with a `canonicalizeDesiredProperties` twin: this is
17641
+ * a SKIP, not a narrowing, so there is no pure function of the desired bag to
17642
+ * fold both comparison sides with, and folding one would derive a REMOVAL of a
17643
+ * live capacity block from a template whose only fault is `BillingMode`.
17644
+ *
17645
+ * Pure and non-mutating at every level.
17646
+ */
17647
+ function retainUnsendableCapacityMembers(desired, previous, keptBillingMode) {
17648
+ const unsendable = keptBillingMode === "PROVISIONED" ? ON_DEMAND_ONLY_CAPACITY_KEYS : PROVISIONED_ONLY_CAPACITY_KEYS;
17649
+ const result = retainCapacityKeys({ ...desired }, previous, unsendable.table);
17650
+ const indexes = result["GlobalSecondaryIndexes"];
17651
+ if (Array.isArray(indexes)) {
17652
+ const previousIndexes = indexCapacityContainers(previous["GlobalSecondaryIndexes"], "IndexName");
17653
+ result["GlobalSecondaryIndexes"] = indexes.map((entry) => {
17654
+ const gsi = asRecord$1(entry);
17655
+ if (!gsi) return entry;
17656
+ const previousGsi = previousIndexes.get(gsi["IndexName"]);
17657
+ return retainCapacityKeys({ ...gsi }, previousGsi, unsendable.index);
17658
+ });
17659
+ }
17660
+ const replicas = result["Replicas"];
17661
+ if (Array.isArray(replicas)) {
17662
+ const previousReplicas = indexCapacityContainers(previous["Replicas"], "Region");
17663
+ result["Replicas"] = replicas.map((entry) => {
17664
+ const replica = asRecord$1(entry);
17665
+ if (!replica) return entry;
17666
+ const previousReplica = previousReplicas.get(replica["Region"]);
17667
+ const copy = retainCapacityKeys({ ...replica }, previousReplica, unsendable.replica);
17668
+ const replicaIndexes = copy["GlobalSecondaryIndexes"];
17669
+ if (Array.isArray(replicaIndexes)) {
17670
+ const previousReplicaIndexes = indexCapacityContainers(previousReplica?.["GlobalSecondaryIndexes"], "IndexName");
17671
+ const kept = replicaIndexes.map((indexEntry) => {
17672
+ const gsi = asRecord$1(indexEntry);
17673
+ if (!gsi) return indexEntry;
17674
+ const previousGsi = previousReplicaIndexes.get(gsi["IndexName"]);
17675
+ return retainCapacityKeys({ ...gsi }, previousGsi, unsendable.replicaIndex);
17676
+ }).filter((entry) => {
17677
+ const gsi = asRecord$1(entry);
17678
+ if (!gsi) return true;
17679
+ return Object.keys(gsi).some((k) => k !== "IndexName");
17680
+ });
17681
+ if (kept.length > 0) copy["GlobalSecondaryIndexes"] = kept;
17682
+ else delete copy["GlobalSecondaryIndexes"];
17683
+ }
17684
+ return copy;
17685
+ });
17686
+ }
17687
+ return result;
17688
+ }
17689
+ /**
17690
+ * Retain (or drop) one container's unsendable capacity members, in place on a
17691
+ * bag the caller already copied.
17692
+ *
17693
+ * The counterpart container being `undefined` — no matching index / replica in
17694
+ * the previous record — is the ABSENT case, not a special one: there is nothing
17695
+ * to vouch for, so the key is dropped exactly as it is for an unusable value.
17696
+ */
17697
+ function retainCapacityKeys(target, previousContainer, keys) {
17698
+ for (const key of keys) {
17699
+ const previousValue = asRecord$1(previousContainer?.[key]);
17700
+ if (previousValue) target[key] = { ...previousValue };
17701
+ else delete target[key];
17702
+ }
17703
+ return target;
17704
+ }
17705
+ /**
17706
+ * Index a previous-side array by its identity member, so a per-index /
17707
+ * per-replica retain reads the counterpart entry rather than the positional
17708
+ * one — AWS does not guarantee readback order and a template edit can reorder
17709
+ * either list.
17710
+ */
17711
+ function indexCapacityContainers(value, identityKey) {
17712
+ const byIdentity = /* @__PURE__ */ new Map();
17713
+ if (!Array.isArray(value)) return byIdentity;
17714
+ for (const entry of value) {
17715
+ const record = asRecord$1(entry);
17716
+ const identity = record?.[identityKey];
17717
+ if (record && typeof identity === "string") byIdentity.set(identity, record);
17718
+ }
17719
+ return byIdentity;
17720
+ }
17721
+ /**
17536
17722
  * True when a CFn sub-object is nothing but an unresolved intrinsic
17537
17723
  * (`{"Fn::If": […]}` / `{"Ref": …}`), i.e. the template DID declare the block
17538
17724
  * but its value could not be resolved to a shape with members.
@@ -20380,6 +20566,43 @@ const SG_INGRESS_IP_PROTOCOL_DEFAULT = "-1";
20380
20566
  /** The refusal path both the create guard and the canonicalizer report under. */
20381
20567
  const SG_INGRESS_IP_PROTOCOL_PATH = "AWS::EC2::SecurityGroupIngress IpProtocol";
20382
20568
  /**
20569
+ * Page ceiling for {@link EC2Provider.lookupIngressRuleId}'s
20570
+ * `DescribeSecurityGroupRules` walk (issue #1761).
20571
+ *
20572
+ * The walk asks for {@link SG_RULE_PAGE_SIZE} rules per page, so 20 pages cover
20573
+ * 20,000 rules on a single security group — far beyond any real one (AWS's
20574
+ * default quota is 60 rules per SG, 1000 at the maximum quota increase). That
20575
+ * arithmetic only holds because the request pins `MaxResults`; without it AWS
20576
+ * applies its own default page size and the bound would mean an unknown number
20577
+ * of rules. The bound exists only so a pathological / looping `NextToken` cannot
20578
+ * hang a deploy; reaching it records nothing rather than answering from a
20579
+ * partial view.
20580
+ */
20581
+ const MAX_SG_RULE_PAGES = 20;
20582
+ /**
20583
+ * `MaxResults` for that walk. `DescribeSecurityGroupRules` accepts 5-1000; the
20584
+ * ceiling is what makes {@link MAX_SG_RULE_PAGES}'s "20,000 rules" true and
20585
+ * keeps the common case (a group with a handful of rules) to ONE round trip.
20586
+ */
20587
+ const SG_RULE_PAGE_SIZE = 1e3;
20588
+ /**
20589
+ * The `sgr-…` security-group rule id, anchored.
20590
+ *
20591
+ * The twin of `SG_RULE_ID_PATTERN` in `src/cli/commands/export.ts`, restated
20592
+ * rather than imported for the same reason that file restates
20593
+ * `ROUTE_DESTINATION_KEYS` instead of importing it from here: neither module may
20594
+ * pull the other's graph (the export command would drag in this provider and its
20595
+ * SDK client).
20596
+ *
20597
+ * Anchoring is what discriminates. `startsWith('sgr-')` also accepts
20598
+ * `sgr-0abc|tcp|443|443` — a composite wearing the identifier's prefix — which
20599
+ * on the import path reaches AWS as a malformed id and raises
20600
+ * `InvalidSecurityGroupRuleId.Malformed`, an error `isNotFoundError` does not
20601
+ * match, so `verifyExplicit` would THROW and abort the whole `cdkd import` run
20602
+ * instead of declining that one row.
20603
+ */
20604
+ const SG_RULE_ID_PATTERN$1 = /^sgr-[0-9a-f]+$/;
20605
+ /**
20383
20606
  * Resolve the `IpProtocol` an `AWS::EC2::SecurityGroupIngress` rule actually
20384
20607
  * SENDS, plus the bag that describes it (issue #1633).
20385
20608
  *
@@ -21713,25 +21936,29 @@ var EC2Provider = class {
21713
21936
  }
21714
21937
  ], onUnusableProtocol ? { onRefusal: onUnusableProtocol } : void 0);
21715
21938
  try {
21716
- await this.ec2Client.send(new AuthorizeSecurityGroupIngressCommand({
21939
+ const ruleId = singleSecurityGroupRuleId((await this.ec2Client.send(new AuthorizeSecurityGroupIngressCommand({
21717
21940
  GroupId: groupId,
21718
21941
  IpPermissions: [{
21719
21942
  ...this.buildIpPermission(properties),
21720
21943
  IpProtocol: ipProtocol
21721
21944
  }]
21722
- }));
21945
+ }))).SecurityGroupRules);
21723
21946
  this.logger.debug(`Successfully created SecurityGroupIngress ${logicalId}: ${physicalId}`);
21724
21947
  return {
21725
21948
  physicalId,
21726
- attributes: {},
21949
+ attributes: ruleId ? { Id: ruleId } : {},
21727
21950
  ...effectiveProperties && { effectiveProperties }
21728
21951
  };
21729
21952
  } catch (error) {
21730
21953
  if (error instanceof Error && error.message.includes("already exists")) {
21731
21954
  this.logger.debug(`SecurityGroupIngress ${logicalId} already exists, treating as success`);
21955
+ const existingRuleId = await this.lookupIngressRuleId(logicalId, groupId, {
21956
+ ...properties,
21957
+ IpProtocol: ipProtocol
21958
+ });
21732
21959
  return {
21733
21960
  physicalId,
21734
- attributes: {},
21961
+ attributes: existingRuleId ? { Id: existingRuleId } : {},
21735
21962
  ...effectiveProperties && { effectiveProperties }
21736
21963
  };
21737
21964
  }
@@ -21739,6 +21966,70 @@ var EC2Provider = class {
21739
21966
  throw new ProvisioningError(`Failed to create SecurityGroupIngress ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
21740
21967
  }
21741
21968
  }
21969
+ /**
21970
+ * Best-effort lookup of the `sgr-...` id of an ALREADY-EXISTING ingress rule
21971
+ * (issue #1761).
21972
+ *
21973
+ * Only the idempotent "already exists" arm of {@link createSecurityGroupIngress}
21974
+ * calls this: the successful arm reads the id straight off
21975
+ * `AuthorizeSecurityGroupIngress`'s own response, which is what keeps the
21976
+ * mutating path free of the issue #1710 orphan hazard.
21977
+ *
21978
+ * **Never throws.** A `DescribeSecurityGroupRules` failure (permissions,
21979
+ * throttle, a rule that no longer matches) degrades to `undefined`, which
21980
+ * records no attribute — `cdkd export` then refuses that one resource with
21981
+ * its own actionable message, which is strictly better than failing a deploy
21982
+ * that AWS already considers satisfied.
21983
+ *
21984
+ * Returns a value only when EXACTLY ONE non-egress rule on the group matches
21985
+ * the requested `(protocol, ports, source)` triple. Zero matches means the
21986
+ * "already exists" error came from a rule cdkd cannot pin down; two or more
21987
+ * means the identifier would be ambiguous, and adopting an ambiguous id is
21988
+ * the #1658 failure mode (a state row that looks adopted but names the wrong
21989
+ * AWS object).
21990
+ *
21991
+ * **Paginated, unlike every other `Describe*` in this file.** That is not an
21992
+ * inconsistency to tidy away: the "exactly one" verdict above is a statement
21993
+ * about the WHOLE group, so a truncated first page turns a genuine 2-match
21994
+ * into a false "exactly one" and records the wrong `sgr-` id — the precise
21995
+ * outcome the ambiguity fence exists to prevent. A group can hold thousands
21996
+ * of rules (the account-level quota is per-SG, not per-page), so truncation
21997
+ * is reachable. `MAX_SG_RULE_PAGES` bounds the walk; exhausting it is treated
21998
+ * as "could not see the whole group" and records nothing, same as a throw.
21999
+ */
22000
+ async lookupIngressRuleId(logicalId, groupId, properties) {
22001
+ try {
22002
+ const rules = [];
22003
+ let nextToken;
22004
+ let pages = 0;
22005
+ do {
22006
+ if (pages >= MAX_SG_RULE_PAGES) {
22007
+ this.logger.debug(`SecurityGroupIngress ${logicalId}: DescribeSecurityGroupRules on ${groupId} still paginating after ${MAX_SG_RULE_PAGES} pages — cannot prove the match is unique, so not recording an Id attribute`);
22008
+ return;
22009
+ }
22010
+ const resp = await this.ec2Client.send(new DescribeSecurityGroupRulesCommand({
22011
+ Filters: [{
22012
+ Name: "group-id",
22013
+ Values: [groupId]
22014
+ }],
22015
+ MaxResults: SG_RULE_PAGE_SIZE,
22016
+ ...nextToken && { NextToken: nextToken }
22017
+ }));
22018
+ rules.push(...resp.SecurityGroupRules ?? []);
22019
+ nextToken = resp.NextToken;
22020
+ pages += 1;
22021
+ } while (nextToken);
22022
+ const ids = rules.filter((rule) => rule.IsEgress !== true && securityGroupRuleMatchesCfnIngress(rule, properties)).map((rule) => rule.SecurityGroupRuleId).filter((id) => typeof id === "string" && id.trim() !== "");
22023
+ if (ids.length !== 1) {
22024
+ this.logger.debug(`SecurityGroupIngress ${logicalId}: ${ids.length} existing rule(s) on ${groupId} match the requested protocol/ports/source — not recording an Id attribute`);
22025
+ return;
22026
+ }
22027
+ return ids[0];
22028
+ } catch (error) {
22029
+ this.logger.debug(`SecurityGroupIngress ${logicalId}: could not look up the existing rule id on ${groupId} (${error instanceof Error ? error.message : String(error)}) — not recording an Id attribute`);
22030
+ return;
22031
+ }
22032
+ }
21742
22033
  async updateSecurityGroupIngress(logicalId, physicalId, resourceType, properties, previousProperties) {
21743
22034
  this.logger.debug(`Updating SecurityGroupIngress ${logicalId}: ${physicalId}`);
21744
22035
  if (JSON.stringify(properties) === JSON.stringify(previousProperties)) return {
@@ -22681,13 +22972,28 @@ var EC2Provider = class {
22681
22972
  /**
22682
22973
  * Adopt an existing EC2 networking resource into cdkd state.
22683
22974
  *
22684
- * Supported types: `AWS::EC2::VPC`, `AWS::EC2::Subnet`,
22685
- * `AWS::EC2::SecurityGroup`. Other EC2 types this provider creates
22686
- * (RouteTable, Route, InternetGateway, VPCGatewayAttachment,
22687
- * NetworkAcl, Instance) return `null` from import — most have no
22688
- * stable identity to look up by tag (Routes are derived; SGIngress
22689
- * is rule-level), and the typical adoption story is "find the VPC,
22690
- * cdkd reconstructs the rest at deploy time".
22975
+ * Every arm here is OVERRIDE-ONLY: `import()` returns `null` unless the
22976
+ * caller passed `--resource <logicalId>=<physicalId>`, and `verifyExplicit`
22977
+ * then confirms that id against AWS. Supported types:
22978
+ *
22979
+ * - `AWS::EC2::VPC`, `AWS::EC2::Subnet`, `AWS::EC2::SecurityGroup` verified
22980
+ * by id, recorded verbatim with no attributes.
22981
+ * - `AWS::EC2::NatGateway` verified by id, ignoring a gateway already in
22982
+ * `deleted` / `deleting` state.
22983
+ * - `AWS::EC2::EIP` — accepts an allocation id, a public IP, or the composite
22984
+ * `<publicIp>|<allocationId>`, and records the composite plus the
22985
+ * `AllocationId` / `PublicIp` attributes.
22986
+ * - `AWS::EC2::SecurityGroupIngress` — accepts the `sgr-...` rule id
22987
+ * (CloudFormation's own identifier for the type), declines an EGRESS rule
22988
+ * id, and records cdkd's composite `<groupId>|<ipProtocol>|<fromPort>|<toPort>`
22989
+ * plus the rule id as the `Id` attribute (issue #1761).
22990
+ *
22991
+ * The remaining EC2 types this provider creates (RouteTable, Route,
22992
+ * InternetGateway, VPCGatewayAttachment, NetworkAcl, NetworkAclEntry,
22993
+ * SubnetRouteTableAssociation, SubnetNetworkAclAssociation, Instance) return
22994
+ * `null` — Routes and associations are derived from their parent, and the
22995
+ * typical adoption story is "find the VPC, cdkd reconstructs the rest at
22996
+ * deploy time".
22691
22997
  *
22692
22998
  * There is no `aws:cdk:path` tag lookup: AWS rejects `aws:`-prefixed tag
22693
22999
  * writes, so that tag never exists on a real resource and a
@@ -23273,6 +23579,39 @@ var EC2Provider = class {
23273
23579
  physicalId,
23274
23580
  attributes: {}
23275
23581
  } : null;
23582
+ case "AWS::EC2::SecurityGroupIngress": {
23583
+ if (!SG_RULE_ID_PATTERN$1.test(physicalId)) return null;
23584
+ const rule = (await this.ec2Client.send(new DescribeSecurityGroupRulesCommand({ SecurityGroupRuleIds: [physicalId] }))).SecurityGroupRules?.[0];
23585
+ if (!rule || rule.IsEgress === true) return null;
23586
+ if (!rule.GroupId || !rule.IpProtocol || !rule.SecurityGroupRuleId) return null;
23587
+ const segments = [
23588
+ {
23589
+ name: "groupId",
23590
+ value: rule.GroupId
23591
+ },
23592
+ {
23593
+ name: "ipProtocol",
23594
+ value: rule.IpProtocol
23595
+ },
23596
+ {
23597
+ name: "fromPort",
23598
+ value: rule.FromPort ?? "-1"
23599
+ },
23600
+ {
23601
+ name: "toPort",
23602
+ value: rule.ToPort ?? "-1"
23603
+ }
23604
+ ];
23605
+ const refusal = compositeIdSeparatorRefusal("AWS::EC2::SecurityGroupIngress", logicalId, segments);
23606
+ if (refusal !== void 0) {
23607
+ this.logger.warn(`${refusal} Skipping import.`);
23608
+ return null;
23609
+ }
23610
+ return {
23611
+ physicalId: packCompositeId("AWS::EC2::SecurityGroupIngress", logicalId, segments),
23612
+ attributes: { Id: rule.SecurityGroupRuleId }
23613
+ };
23614
+ }
23276
23615
  case "AWS::EC2::NatGateway": return (await this.ec2Client.send(new DescribeNatGatewaysCommand({ NatGatewayIds: [physicalId] }))).NatGateways?.find((g) => g.State !== "deleted" && g.State !== "deleting") ? {
23277
23616
  physicalId,
23278
23617
  attributes: {}
@@ -23393,6 +23732,93 @@ function flattenIpPermissions(perms, direction) {
23393
23732
  function sgProtocolKey(value) {
23394
23733
  return canonicalizeIpProtocolValue(value ?? "-1");
23395
23734
  }
23735
+ /**
23736
+ * The single `sgr-...` id an `AuthorizeSecurityGroupIngress` response reports
23737
+ * for a standalone `AWS::EC2::SecurityGroupIngress` (issue #1761).
23738
+ *
23739
+ * Pure by design: {@link EC2Provider.createSecurityGroupIngress} calls it
23740
+ * between the mutating Authorize and its return, where an `await` of anything
23741
+ * that can throw would re-create the issue #1710 orphan class.
23742
+ *
23743
+ * `undefined` unless the response carries EXACTLY ONE rule id. AWS mints one
23744
+ * rule per source entry, and cdkd sends one `IpPermission` per CFn resource —
23745
+ * but a template declaring both `CidrIp` and `CidrIpv6` on the same ingress
23746
+ * resource makes `buildIpPermission` emit two sources and AWS answers with two
23747
+ * rules. Neither is "the" CloudFormation identifier for the resource, so
23748
+ * recording nothing (and letting `cdkd export` refuse with its own message) is
23749
+ * the only answer that cannot silently name the wrong rule.
23750
+ */
23751
+ function singleSecurityGroupRuleId(rules) {
23752
+ const ids = (rules ?? []).map((rule) => rule.SecurityGroupRuleId).filter((id) => typeof id === "string" && id.trim() !== "");
23753
+ return ids.length === 1 ? ids[0] : void 0;
23754
+ }
23755
+ /**
23756
+ * A CFn `FromPort` / `ToPort` in the NUMBER form `DescribeSecurityGroupRules`
23757
+ * reports, for {@link securityGroupRuleMatchesCfnIngress}'s compare.
23758
+ *
23759
+ * An identity compare against `properties['FromPort'] as number` was wrong for
23760
+ * the same reason issue #1513 documents one property over: a YAML / JSON
23761
+ * template may spell a port as the STRING `"443"`, and `buildIpPermission`
23762
+ * passes it through to AWS unchanged, so such a rule deploys fine and would
23763
+ * then never match its own AWS-side row.
23764
+ *
23765
+ * Absent, `null`, and blank (INCLUDING whitespace-only) are AWS's `-1`, "all
23766
+ * ports". Everything that is not a number or a numeric string is `NaN`, which
23767
+ * compares unequal to everything — so the lookup records nothing rather than
23768
+ * matching some unrelated rule.
23769
+ *
23770
+ * A bare `Number(value)` was NOT enough for that last sentence, which an earlier
23771
+ * cut of this function claimed anyway: `Number` maps `' '` and `[]` to `0`,
23772
+ * `true` to `1`, and `['443']` to `443`. A whitespace-only port would then have
23773
+ * matched an ICMP **type-0** rule — a different rule, silently recorded — which
23774
+ * is the exact outcome the claim promised it could not produce.
23775
+ */
23776
+ function cfnIngressPortValue(value) {
23777
+ if (value === void 0 || value === null) return -1;
23778
+ if (typeof value === "number") return Number.isFinite(value) ? value : NaN;
23779
+ if (typeof value === "string") {
23780
+ const trimmed = value.trim();
23781
+ if (trimmed === "") return -1;
23782
+ const parsed = Number(trimmed);
23783
+ return Number.isFinite(parsed) ? parsed : NaN;
23784
+ }
23785
+ return NaN;
23786
+ }
23787
+ /**
23788
+ * Does a `DescribeSecurityGroupRules` rule describe the same ingress rule the
23789
+ * given CFn `AWS::EC2::SecurityGroupIngress` properties ask for?
23790
+ *
23791
+ * Deliberately NOT expressed via {@link sgRuleKey}: that key includes
23792
+ * `SourceSecurityGroupOwnerId` and `Description`, both of which AWS fills in on
23793
+ * the read side even when the template omits them, so a whole-key compare
23794
+ * would report "no match" for the ordinary same-account SG-to-SG rule this
23795
+ * lookup exists to find. Matching the fields the template actually pins —
23796
+ * protocol, port range, and whichever ONE source key it declares — is what
23797
+ * makes the comparison decidable from a partial template.
23798
+ *
23799
+ * The protocol goes through {@link sgProtocolKey} on BOTH sides, not a raw
23800
+ * compare: AWS substitutes the canonical NAME for the four protocols that have
23801
+ * one, so a template declaring `IpProtocol: 6` (or `'6'`) is read back as
23802
+ * `'tcp'` and would otherwise match nothing (the issue #1643 fold, one call site
23803
+ * further along).
23804
+ *
23805
+ * Ports are compared with AWS's own `-1` ("all") spelling on both sides, since
23806
+ * an omitted `FromPort` / `ToPort` reads back as `-1`, and the template side is
23807
+ * NUMERICALLY coerced — see {@link cfnIngressPortValue}.
23808
+ */
23809
+ function securityGroupRuleMatchesCfnIngress(rule, properties) {
23810
+ if (sgProtocolKey(rule.IpProtocol) !== sgProtocolKey(properties["IpProtocol"])) return false;
23811
+ if ((rule.FromPort ?? -1) !== cfnIngressPortValue(properties["FromPort"])) return false;
23812
+ if ((rule.ToPort ?? -1) !== cfnIngressPortValue(properties["ToPort"])) return false;
23813
+ const sources = [
23814
+ [properties["CidrIp"], rule.CidrIpv4],
23815
+ [properties["CidrIpv6"], rule.CidrIpv6],
23816
+ [properties["SourceSecurityGroupId"], rule.ReferencedGroupInfo?.GroupId],
23817
+ [properties["SourcePrefixListId"], rule.PrefixListId]
23818
+ ];
23819
+ for (const [declared, actual] of sources) if (declared !== void 0 && declared !== null && declared !== actual) return false;
23820
+ return true;
23821
+ }
23396
23822
  function sgRuleKey(rule, direction) {
23397
23823
  const peerKey = direction === "egress" ? rule["DestinationSecurityGroupId"] : rule["SourceSecurityGroupId"];
23398
23824
  const prefixKey = direction === "egress" ? rule["DestinationPrefixListId"] : rule["SourcePrefixListId"];
@@ -59473,7 +59899,7 @@ function readStringProperty(properties, key, resourceType) {
59473
59899
  * entry reads the value cdkd recorded in state (`attributes`), and refuses when
59474
59900
  * it is absent rather than guessing.
59475
59901
  *
59476
- * ## Live measurement (us-east-1, 2026-08-13)
59902
+ * ## Live measurement (us-east-1, 2026-08-13; the SG-ingress row 2026-08-14)
59477
59903
  *
59478
59904
  * Every field below is BOTH the type's single-field `primaryIdentifier` AND
59479
59905
  * `readOnlyProperties`, which is why each resolution returns an EMPTY
@@ -59485,7 +59911,19 @@ function readStringProperty(properties, key, resourceType) {
59485
59911
  * | `AWS::AppSync::DataSource` | `DataSourceArn` | `attributes.DataSourceArn` |
59486
59912
  * | `AWS::AppSync::Resolver` | `ResolverArn` | `attributes.ResolverArn` |
59487
59913
  * | `AWS::S3Tables::Table` | `TableARN` | `attributes.TableARN` |
59488
- * | `AWS::EC2::SecurityGroupIngress` | `Id` | NOTHING (issue #1761) |
59914
+ * | `AWS::EC2::SecurityGroupIngress` | `Id` | `attributes.Id` (issue #1761) |
59915
+ *
59916
+ * The SG-ingress row is measured the same way as the rest, and separately
59917
+ * against a real CloudFormation stack because the three strings that name one
59918
+ * resource can disagree (issue #1771 found CFn reporting an EIP as the bare
59919
+ * public IP while Cloud Control reports the composite, and the two disagreeing
59920
+ * on field ORDER for `AWS::EC2::VPCGatewayAttachment`). For this type they all
59921
+ * AGREE: `describe-type` says `primaryIdentifier` / `readOnlyProperties` are
59922
+ * both exactly `["/properties/Id"]` with a full `create`/`delete`/`list`/
59923
+ * `read`/`update` handler set and `ProvisioningType: FULLY_MUTABLE`, and a live
59924
+ * stack carrying a standalone ingress rule reports `PhysicalResourceId`
59925
+ * `sgr-02345615af6d2db0d`, with `Ref` and `Fn::GetAtt .Id` both returning that
59926
+ * same value.
59489
59927
  *
59490
59928
  * All four declare a `read` handler, so CFn genuinely does accept them for
59491
59929
  * IMPORT — unlike `AWS::Glue::Table`, the type issue #1659's title names, which
@@ -59510,13 +59948,70 @@ const COMPOSITE_PHYSICAL_ID_IDENTIFIERS = {
59510
59948
  physicalIdShape: "<tableBucketARN>|<namespace>|<name>",
59511
59949
  arnServiceSegment: "s3tables"
59512
59950
  }),
59513
- "AWS::EC2::SecurityGroupIngress": {
59951
+ "AWS::EC2::SecurityGroupIngress": recordedRuleIdIdentifier()
59952
+ };
59953
+ /**
59954
+ * The `sgr-…` shape, anchored. Shape-only — the hex body's LENGTH is not
59955
+ * pinned, because this exists to tell the identifier apart from the two other
59956
+ * strings that can sit in the same state row, not to validate an id AWS itself
59957
+ * minted:
59958
+ *
59959
+ * - cdkd's own composite physicalId, which starts `sg-` (the parent group) and
59960
+ * carries `|` separators; and
59961
+ * - a bare `sg-…` group id, which a hand-edited row could put under
59962
+ * `attributes.Id`.
59963
+ *
59964
+ * Anchoring is what does the discriminating: `^sgr-[0-9a-f]+$` cannot match
59965
+ * either, because `|` and the composite's trailing segments have nowhere to go.
59966
+ * A bare `startsWith('sgr-')` would accept `sgr-abc|tcp|443|443`.
59967
+ *
59968
+ * Measured live (us-east-1, 2026-08-14) from a CloudFormation stack carrying a
59969
+ * standalone ingress rule: `PhysicalResourceId` = `sgr-02345615af6d2db0d`, and
59970
+ * both `Ref` and `Fn::GetAtt .Id` return that same value.
59971
+ */
59972
+ const SG_RULE_ID_PATTERN = /^sgr-[0-9a-f]+$/;
59973
+ /**
59974
+ * The {@link CompositePhysicalIdIdentifier} for `AWS::EC2::SecurityGroupIngress`.
59975
+ *
59976
+ * The `recordedArnIdentifier` sibling of the family, differing only in that the
59977
+ * identifier is an `sgr-` rule id rather than an ARN — so it gets its own
59978
+ * builder rather than an `isUsable` callback threaded through that one, which
59979
+ * would make the ARN family's every message ("which is not a <service> ARN",
59980
+ * "so cdkd records <field>") conditional on a shape it no longer owns.
59981
+ *
59982
+ * Two accepted sources, validated by the SAME predicate — the point #1771
59983
+ * made about the EIP splitter's segment binding applies here verbatim:
59984
+ * validating one side is not a discriminator.
59985
+ *
59986
+ * 1. `attributes.Id`, which every writer produces today — a fresh deploy, a
59987
+ * `cdkd import --resource <logicalId>=sgr-…`, and the recursive
59988
+ * `--migrate-from-cloudformation` walk (which hands CFn's
59989
+ * `PhysicalResourceId` to `EC2Provider.import` as `knownPhysicalId`, and
59990
+ * that value IS the `sgr-` id — measured above).
59991
+ * 2. A physicalId that is ALREADY the bare rule id. **No cdkd path writes this
59992
+ * shape today** — unlike `AWS::S3Tables::Table`, whose Cloud-Control import
59993
+ * genuinely records the bare ARN — so it is accepted defensively rather than
59994
+ * because it is live: CFn's `PhysicalResourceId` for the type is exactly
59995
+ * this string, so a row hand-repaired from a CFn console listing, or written
59996
+ * by some future Cloud-Control routing of the type, carries an unambiguous
59997
+ * identifier that it would be perverse to refuse. Such a row is separately
59998
+ * BROKEN for `cdkd destroy` (`deleteSecurityGroupIngress` needs the 4-part
59999
+ * tuple), which is another reason to let the export hand it to CFn rather
60000
+ * than strand it.
60001
+ */
60002
+ function recordedRuleIdIdentifier() {
60003
+ const isUsableIdentifier = (value) => SG_RULE_ID_PATTERN.test(value.trim());
60004
+ return {
59514
60005
  field: "Id",
59515
- resolve: ({ logicalId }) => {
59516
- throw new Error(`cdkd's physical id for AWS::EC2::SecurityGroupIngress is the composite '<groupId>|<ipProtocol>|<fromPort>|<toPort>', but CloudFormation IMPORT identifies the rule by the 'sgr-...' security-group rule id AWS assigns, which cdkd does not record in state (tracked in https://github.com/go-to-k/cdkd/issues/1761). Remove '${logicalId}' from the stack before exporting (the rule stays in AWS and can be re-declared in CloudFormation afterwards), or destroy it first and let CloudFormation create it fresh.`);
60006
+ resolve: ({ logicalId, physicalId, attributes }) => {
60007
+ const recorded = attributes["Id"];
60008
+ if (typeof recorded === "string" && isUsableIdentifier(recorded)) return recorded.trim();
60009
+ if (isUsableIdentifier(physicalId)) return physicalId.trim();
60010
+ const recordedNote = typeof recorded === "string" && recorded.trim() ? `attributes.Id is recorded as '${recorded.trim()}', which is not an 'sgr-…' security-group rule id` : `attributes.Id is missing or empty`;
60011
+ throw new Error(`cdkd's physical id for AWS::EC2::SecurityGroupIngress is the composite '<groupId>|<ipProtocol>|<fromPort>|<toPort>', but CloudFormation IMPORT identifies the rule by the 'sgr-...' security-group rule id AWS assigns, which cdkd state does not record usably for '${logicalId}' (${recordedNote}). Two causes, with different remedies. (1) The rule declares MORE THAN ONE source — e.g. both CidrIp and CidrIpv6 on the same resource — so AWS minted one rule per source and cdkd deliberately recorded neither id, because neither is 'the' identifier for this resource. Re-deploying does not help; split it into one AWS::EC2::SecurityGroupIngress resource per source, which is also the shape CloudFormation will manage after the export. (2) Otherwise the rule was deployed by a cdkd older than https://github.com/go-to-k/cdkd/issues/1761, which did not record the id at all. AWS returns it only from AuthorizeSecurityGroupIngress itself, so a no-op re-deploy will NOT heal the record either: change any property of the rule (cdkd revokes and re-authorizes, minting a fresh id, with a momentary traffic interruption) or destroy and re-deploy it, then re-run cdkd export. In both cases you can instead remove '${logicalId}' from the stack before exporting — the rule stays in AWS and can be re-declared in CloudFormation afterwards.`);
59517
60012
  }
59518
- }
59519
- };
60013
+ };
60014
+ }
59520
60015
  /**
59521
60016
  * Build the {@link CompositePhysicalIdIdentifier} for a type whose CFn
59522
60017
  * identifier is an ARN cdkd records in `attributes` under the SAME name as the
@@ -75943,7 +76438,7 @@ function createMigrateCommand() {
75943
76438
  */
75944
76439
  function buildProgram() {
75945
76440
  const program = new Command();
75946
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.282.7");
76441
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.1");
75947
76442
  program.hook("preAction", (_thisCommand, actionCommand) => {
75948
76443
  const { profile } = actionCommand.optsWithGlobals();
75949
76444
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;