@go-to-k/cdkd 0.284.16 → 0.284.17

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 isTerminationProtectionPropagationError, $n as ResourceUpdateNotSupportedError, $t as validateAssetBucketName, A as renderStatefulReason, An as derivePartitionAndUrlSuffix, At as INTRINSIC_KEYS, B as exportAliasCollisionScrubWarning, Bn as CdkdError, Bt as stringifyValue, C as isFinalSnapshotError, Cn as CFN_TEMPLATE_URL_LIMIT, Ct as s3BucketArn, D as extractDeploymentEventError, Dn as expectedOwnerParam, Dt as s3BucketWebsiteUrl, E as makeCanonicalizePropertiesFn, En as uploadCfnTemplate, Et as s3BucketRegionalDomainName, F as green, Fn as AwsClients, Ft as LockManager, G as collectInlinePolicyNamesManagedBySiblings, Gn as LocalMigrateError, Gt as rewriteTemplateAssetReferences, H as secretBearingStateKeyWarning, Ht as buildAssetRedirectMap, I as red, In as getAwsClients, It as S3StateBackend, J as findActionableSilentDrops, Jn as MissingCdkCliError, Jt as AssetModeResolver, K as clearOnUpdateRemoval, Kn as LocalStartServiceError, Kt as escapeRegExp, L as yellow, Ln as resetAwsClients, Lt as rebuildClientForBucketRegion, M as bold, Mn as processStackMessages, Mt as withRetry, N as cyan, Nt as DagBuilder, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as PARTITION_TABLE, Ot as applyRoleArnIfSet, P as gray, Pn as resolveBucketRegion, Pt as TemplateParser, Q as disableInstanceApiTermination, Qn as ResourceTimeoutError, Qt as parseBootstrapMarker, R as collectDeclaredOutputNames, Rn as setAwsClients, Rt as shouldRetainResource, S as createPreDeleteFinalSnapshot, Sn as CFN_TEMPLATE_BODY_LIMIT, St as scrubResourceRecord, T as unsupportedFinalSnapshotError, Tn as findLargeInlineResources, Tt as s3BucketDualStackDomainName, U as stateKeySecretExposure, Un as DeployCancelledError, Ut as createAssetRedirectResolver, V as isExportAliasCollision, Vt as WorkGraph, W as IAMRoleProvider, Wn as LocalInvokeBuildError$1, Wt as loadPublishableAssetManifest, X as CloudControlProvider, Xn as PartialFailureError, Xt as ensureAssetStorage, Y as findSilentDropProperties, Yn as NestedStackChildDirectDestroyError, Yt as BOOTSTRAP_MARKER_PREFIX, Z as slowCcOperationTimeoutMs, Zn as ProvisioningError, Zt as getBootstrapMarkerKey, _ as computeImplicitDeleteEdges, _n as resolveStateBucketWithDefault, _t as STATE_SOURCED_READBACK_RULES, a as DeploymentEventsStore, an as runDockerForeground, at as normalizeAwsTagsToCfn, b as buildFinalSnapshotIdentifier, bn as stateBucketExistenceConfirmed, bt as maskSecretsInText, c as replayFailedOperations, cn as getDockerImageBySourceHash, cr as isMarkedNonRetryable, ct as coerceCfnBoolean, d as updatePartialReason, dn as getDefaultStateBucketName, dr as markNonRetryable, dt as readConfigString, en as validateContainerRepoName, er as StackHasActiveImportsError, et as IntrinsicFunctionResolver, f as UNSPECIFIED_SKIP_REASON, fn as getLegacyStateBucketName, ft as replayWarn, g as IMPLICIT_DELETE_DEPENDENCIES, gn as resolveSkipPrefix, gt as STATE_SOURCED_CROSS_GENERATION_RULES, h as maskingRetryLogger, hn as resolveCaptureObservedState, ht as requireConfigString, i as DeploymentEventsReader, in as getDockerCmd, it as WAFv2WebACLProvider, j as formatResourceLine, jn as AssemblyReader, jt as describeTypeWithThrottleRetry, k as isStatefulRecreateTargetSync, kn as canonicalizeRegion, kt as DiffCalculator, l as replayRollback, ln as Synthesizer, lr as isRetryableTransientError, lt as configBooleanRefusal, m as withResourceDeadline, mn as resolveAutoAssetStorage, mt as requireConfigObject, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as buildDockerImage, nt as getAccountInfo, o as planFailedOps, on as runDockerStreaming, or as normalizeAwsError, ot as resolveExplicitPhysicalId, p as deleteSkipReason, pn as resolveApp, pt as requireConfigArray, q as ProviderRegistry, qt as stripControlChars, r as DeployEngine, rn as formatDockerLoginError, rt as refStateLookupFromResource, s as planRollback, sn as AssetManifestLoader, sr as withErrorHandling, st as assertRegionMatch, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as buildDenyExternalAccessPolicy, tr as StackTerminationProtectionError, tt as cfnRefValueFromPhysicalId, u as updatePartialMessage, un as synthesisStatusMessage, ur as isThrottlingError, ut as configStringRefusal, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as resolveStateBucketWithDefaultAndSource, vt as TEMPLATE_SOURCED_RULES, w as refusesFinalSnapshot, wn as MIGRATE_TMP_PREFIX, wt as s3BucketDomainName, x as ccRoutedFinalSnapshotError, xn as warnDeprecatedNoPrefixCliFlag, xt as redactSecretsForState, y as PRE_DELETE_SNAPSHOT_TYPES, yn as resolveUseCdkBootstrapAssets, yt as createSecretMasker, z as collectPublishedOutputNames, zt as AssetPublisher } from "./deploy-engine-CI0a6yLX.js";
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-D7DqNvVo.js";
2
+ import { $ as isTerminationProtectionPropagationError, $n as ResourceUpdateNotSupportedError, $t as validateAssetBucketName, A as renderStatefulReason, An as derivePartitionAndUrlSuffix, At as INTRINSIC_KEYS, B as exportAliasCollisionScrubWarning, Bn as CdkdError, Bt as stringifyValue, C as isFinalSnapshotError, Cn as CFN_TEMPLATE_URL_LIMIT, Ct as s3BucketArn, D as extractDeploymentEventError, Dn as expectedOwnerParam, Dt as s3BucketWebsiteUrl, E as makeCanonicalizePropertiesFn, En as uploadCfnTemplate, Et as s3BucketRegionalDomainName, F as green, Fn as AwsClients, Ft as LockManager, G as collectInlinePolicyNamesManagedBySiblings, Gn as LocalMigrateError, Gt as rewriteTemplateAssetReferences, H as secretBearingStateKeyWarning, Ht as buildAssetRedirectMap, I as red, In as getAwsClients, It as S3StateBackend, J as findActionableSilentDrops, Jn as MissingCdkCliError, Jt as AssetModeResolver, K as clearOnUpdateRemoval, Kn as LocalStartServiceError, Kt as escapeRegExp, L as yellow, Ln as resetAwsClients, Lt as rebuildClientForBucketRegion, M as bold, Mn as processStackMessages, Mt as withRetry, N as cyan, Nt as DagBuilder, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as PARTITION_TABLE, Ot as applyRoleArnIfSet, P as gray, Pn as resolveBucketRegion, Pt as TemplateParser, Q as disableInstanceApiTermination, Qn as ResourceTimeoutError, Qt as parseBootstrapMarker, R as collectDeclaredOutputNames, Rn as setAwsClients, Rt as shouldRetainResource, S as createPreDeleteFinalSnapshot, Sn as CFN_TEMPLATE_BODY_LIMIT, St as scrubResourceRecord, T as unsupportedFinalSnapshotError, Tn as findLargeInlineResources, Tt as s3BucketDualStackDomainName, U as stateKeySecretExposure, Un as DeployCancelledError, Ut as createAssetRedirectResolver, V as isExportAliasCollision, Vt as WorkGraph, W as IAMRoleProvider, Wn as LocalInvokeBuildError$1, Wt as loadPublishableAssetManifest, X as CloudControlProvider, Xn as PartialFailureError, Xt as ensureAssetStorage, Y as findSilentDropProperties, Yn as NestedStackChildDirectDestroyError, Yt as BOOTSTRAP_MARKER_PREFIX, Z as slowCcOperationTimeoutMs, Zn as ProvisioningError, Zt as getBootstrapMarkerKey, _ as computeImplicitDeleteEdges, _n as resolveStateBucketWithDefault, _t as STATE_SOURCED_READBACK_RULES, a as DeploymentEventsStore, an as runDockerForeground, at as normalizeAwsTagsToCfn, b as buildFinalSnapshotIdentifier, bn as stateBucketExistenceConfirmed, bt as maskSecretsInText, c as replayFailedOperations, cn as getDockerImageBySourceHash, cr as isMarkedNonRetryable, ct as coerceCfnBoolean, d as updatePartialReason, dn as getDefaultStateBucketName, dr as markNonRetryable, dt as readConfigString, en as validateContainerRepoName, er as StackHasActiveImportsError, et as IntrinsicFunctionResolver, f as UNSPECIFIED_SKIP_REASON, fn as getLegacyStateBucketName, ft as replayWarn, g as IMPLICIT_DELETE_DEPENDENCIES, gn as resolveSkipPrefix, gt as STATE_SOURCED_CROSS_GENERATION_RULES, h as maskingRetryLogger, hn as resolveCaptureObservedState, ht as requireConfigString, i as DeploymentEventsReader, in as getDockerCmd, it as WAFv2WebACLProvider, j as formatResourceLine, jn as AssemblyReader, jt as describeTypeWithThrottleRetry, k as isStatefulRecreateTargetSync, kn as canonicalizeRegion, kt as DiffCalculator, l as replayRollback, ln as Synthesizer, lr as isRetryableTransientError, lt as configBooleanRefusal, m as withResourceDeadline, mn as resolveAutoAssetStorage, mt as requireConfigObject, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as buildDockerImage, nt as getAccountInfo, o as planFailedOps, on as runDockerStreaming, or as normalizeAwsError, ot as resolveExplicitPhysicalId, p as deleteSkipReason, pn as resolveApp, pt as requireConfigArray, q as ProviderRegistry, qt as stripControlChars, r as DeployEngine, rn as formatDockerLoginError, rt as refStateLookupFromResource, s as planRollback, sn as AssetManifestLoader, sr as withErrorHandling, st as assertRegionMatch, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as buildDenyExternalAccessPolicy, tr as StackTerminationProtectionError, tt as cfnRefValueFromPhysicalId, u as updatePartialMessage, un as synthesisStatusMessage, ur as isThrottlingError, ut as configStringRefusal, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as resolveStateBucketWithDefaultAndSource, vt as TEMPLATE_SOURCED_RULES, w as refusesFinalSnapshot, wn as MIGRATE_TMP_PREFIX, wt as s3BucketDomainName, x as ccRoutedFinalSnapshotError, xn as warnDeprecatedNoPrefixCliFlag, xt as redactSecretsForState, y as PRE_DELETE_SNAPSHOT_TYPES, yn as resolveUseCdkBootstrapAssets, yt as createSecretMasker, z as collectPublishedOutputNames, zt as AssetPublisher } from "./deploy-engine-Bnu1WuRI.js";
3
+ import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as getCurrentStackName, 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-BbLu5LGL.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";
@@ -30,7 +30,7 @@ import * as path from "node:path";
30
30
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
31
31
  import { execFile, spawn } from "node:child_process";
32
32
  import { tmpdir } from "node:os";
33
- import { AssociateVPCWithHostedZoneCommand, ChangeResourceRecordSetsCommand, ChangeTagsForResourceCommand, CreateHostedZoneCommand, CreateQueryLoggingConfigCommand, DeleteHostedZoneCommand, DeleteQueryLoggingConfigCommand, DisassociateVPCFromHostedZoneCommand, GetHostedZoneCommand, ListHostedZonesByNameCommand, ListQueryLoggingConfigsCommand, ListResourceRecordSetsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$8, Route53Client, UpdateHostedZoneCommentCommand, UpdateHostedZoneFeaturesCommand } from "@aws-sdk/client-route-53";
33
+ import { AssociateVPCWithHostedZoneCommand, ChangeResourceRecordSetsCommand, ChangeTagsForResourceCommand, CreateHostedZoneCommand, CreateQueryLoggingConfigCommand, DeleteHostedZoneCommand, DeleteQueryLoggingConfigCommand, DisassociateVPCFromHostedZoneCommand, GetHostedZoneCommand, HostedZoneAlreadyExists, ListHostedZonesByNameCommand, ListQueryLoggingConfigsCommand, ListResourceRecordSetsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$8, Route53Client, UpdateHostedZoneCommentCommand, UpdateHostedZoneFeaturesCommand } from "@aws-sdk/client-route-53";
34
34
  import { AddTagsCommand, CreateListenerCommand, CreateLoadBalancerCommand, CreateTargetGroupCommand, DeleteListenerCommand, DeleteLoadBalancerCommand, DeleteTargetGroupCommand, DeregisterTargetsCommand, DescribeCapacityReservationCommand, DescribeListenerAttributesCommand, DescribeListenersCommand, DescribeLoadBalancerAttributesCommand, DescribeLoadBalancersCommand, DescribeTagsCommand, DescribeTargetGroupAttributesCommand, DescribeTargetGroupsCommand, DescribeTargetHealthCommand, ElasticLoadBalancingV2Client, ModifyCapacityReservationCommand, ModifyIpPoolsCommand, ModifyListenerAttributesCommand, ModifyListenerCommand, ModifyLoadBalancerAttributesCommand, ModifyTargetGroupAttributesCommand, ModifyTargetGroupCommand, RegisterTargetsCommand, RemoveTagsCommand, SetIpAddressTypeCommand, SetSecurityGroupsCommand, SetSubnetsCommand, waitUntilLoadBalancerAvailable } from "@aws-sdk/client-elastic-load-balancing-v2";
35
35
  import { CreateAliasCommand, CreateKeyCommand, DeleteAliasCommand, DescribeKeyCommand, DisableKeyCommand, DisableKeyRotationCommand, EnableKeyCommand, EnableKeyRotationCommand, GetKeyPolicyCommand, GetKeyRotationStatusCommand, KMSClient, ListAliasesCommand, ListResourceTagsCommand, NotFoundException as NotFoundException$2, PutKeyPolicyCommand, ScheduleKeyDeletionCommand, TagResourceCommand as TagResourceCommand$11, UntagResourceCommand as UntagResourceCommand$11, UpdateAliasCommand, UpdateKeyDescriptionCommand } from "@aws-sdk/client-kms";
36
36
  import { join as join$1 } from "path";
@@ -3845,6 +3845,16 @@ var IAMInstanceProfileProvider = class {
3845
3845
  //#endregion
3846
3846
  //#region src/provisioning/providers/iam-access-key-provider.ts
3847
3847
  /**
3848
+ * How far before an attempt's start a key's `CreateDate` may fall and still be
3849
+ * treated as minted by that attempt (issue #2039).
3850
+ *
3851
+ * IAM stamps `CreateDate` from its own clock while the floor comes from ours,
3852
+ * so some tolerance is needed or ordinary skew disarms the check entirely. It
3853
+ * is deliberately small: every millisecond of it is a millisecond in which a
3854
+ * key created by something else becomes deletable.
3855
+ */
3856
+ const CREATE_DATE_SKEW_MARGIN_MS = 5e3;
3857
+ /**
3848
3858
  * AWS IAM AccessKey Provider (issue #1323)
3849
3859
  *
3850
3860
  * Implements resource provisioning for AWS::IAM::AccessKey using the IAM SDK.
@@ -3894,6 +3904,38 @@ var IAMAccessKeyProvider = class {
3894
3904
  "Serial",
3895
3905
  "Status"
3896
3906
  ])]]);
3907
+ /**
3908
+ * Serializes create attempts per IAM user, in this process (issue
3909
+ * [#2039](https://github.com/go-to-k/cdkd/issues/2039)).
3910
+ *
3911
+ * The orphan reconcile below identifies "a key that appeared since my
3912
+ * baseline". With two `AWS::IAM::AccessKey` resources on ONE user — the
3913
+ * ordinary two-key rotation shape — the deploy engine dispatches both
3914
+ * concurrently at the default `--concurrency 10`, and a key the SIBLING
3915
+ * legitimately created lands inside that window and matches the same
3916
+ * description. Deleting it destroys a live credential that is already in
3917
+ * cdkd state and may already be piped into a downstream Secrets Manager
3918
+ * secret via `Fn::GetAtt`, and `readCurrentState` returns `undefined` for a
3919
+ * deleted key, so `cdkd drift` reports UNKNOWN rather than drift and the
3920
+ * loss is invisible. That is strictly worse than the untracked-but-alive
3921
+ * credential this whole mechanism exists to prevent.
3922
+ *
3923
+ * This lock removes that race rather than narrowing it: the baseline read,
3924
+ * the create attempt, and the reconcile all happen under it, so no sibling
3925
+ * create on the same user can interleave with them.
3926
+ */
3927
+ userCreateLocks = /* @__PURE__ */ new Map();
3928
+ /**
3929
+ * Access key ids THIS process created successfully.
3930
+ *
3931
+ * The complement of the baseline: a baseline can only say "this key is
3932
+ * newer than my snapshot", which is true both of the orphan left by my own
3933
+ * failed attempt AND of a key someone else minted in the same window. This
3934
+ * set names the keys cdkd itself owns and has already recorded, so they are
3935
+ * never deletable however the timing falls — including the case the lock
3936
+ * cannot cover, a second `cdkd` process sharing the user.
3937
+ */
3938
+ accessKeyIdsCreatedByThisProcess = /* @__PURE__ */ new Set();
3897
3939
  constructor() {
3898
3940
  const awsClients = getAwsClients();
3899
3941
  this.iamClient = awsClients.iam;
@@ -3912,6 +3954,16 @@ var IAMAccessKeyProvider = class {
3912
3954
  const userName = properties["UserName"];
3913
3955
  if (!userName) throw new ProvisioningError(`AccessKey ${logicalId} requires UserName`, resourceType, logicalId);
3914
3956
  const status = requireConfigString(properties["Status"], "Active", "AWS::IAM::AccessKey Status", replayWarn(this.logger, context));
3957
+ return this.withUserCreateLock(userName, () => this.createAccessKeyUnderUserLock(logicalId, resourceType, userName, status));
3958
+ }
3959
+ /**
3960
+ * The body of {@link IAMAccessKeyProvider.create}, running with this user's
3961
+ * create lock held so the baseline, the create, and the reconcile are atomic
3962
+ * against any sibling create on the same user.
3963
+ */
3964
+ async createAccessKeyUnderUserLock(logicalId, resourceType, userName, status) {
3965
+ const attemptStartMs = Date.now() - CREATE_DATE_SKEW_MARGIN_MS;
3966
+ const baseline = await this.tryListAccessKeyIds(userName, logicalId);
3915
3967
  try {
3916
3968
  const response = await this.iamClient.send(new CreateAccessKeyCommand({ UserName: userName }));
3917
3969
  const accessKeyId = response.AccessKey?.AccessKeyId;
@@ -3946,6 +3998,7 @@ var IAMAccessKeyProvider = class {
3946
3998
  throw innerError;
3947
3999
  }
3948
4000
  this.logger.debug(`Successfully created IAM access key ${logicalId}: ${accessKeyId}`);
4001
+ this.accessKeyIdsCreatedByThisProcess.add(accessKeyId);
3949
4002
  return {
3950
4003
  physicalId: accessKeyId,
3951
4004
  attributes: {
@@ -3954,11 +4007,135 @@ var IAMAccessKeyProvider = class {
3954
4007
  }
3955
4008
  };
3956
4009
  } catch (error) {
4010
+ await this.deleteOrphanFromFailedAttempt(userName, logicalId, baseline, attemptStartMs);
3957
4011
  const cause = error instanceof Error ? error : void 0;
3958
4012
  throw new ProvisioningError(`Failed to create IAM access key ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
3959
4013
  }
3960
4014
  }
3961
4015
  /**
4016
+ * Run `fn` with this IAM user's create lock held.
4017
+ *
4018
+ * Queue rather than mutual exclusion: each caller waits on its predecessor
4019
+ * and publishes its own completion for the next one, so N concurrent creates
4020
+ * on one user run strictly in arrival order. The stored promise only ever
4021
+ * RESOLVES (the release is in a `finally`), so one create failing cannot
4022
+ * wedge the queue behind it.
4023
+ *
4024
+ * Process-scoped, like every other guard here. A second `cdkd` process on the
4025
+ * same user is out of its reach, which is why
4026
+ * {@link IAMAccessKeyProvider.accessKeyIdsCreatedByThisProcess} and the
4027
+ * `CreateDate` floor exist as well.
4028
+ */
4029
+ async withUserCreateLock(userName, fn) {
4030
+ const predecessor = this.userCreateLocks.get(userName) ?? Promise.resolve();
4031
+ let release;
4032
+ const held = new Promise((resolve) => {
4033
+ release = resolve;
4034
+ });
4035
+ this.userCreateLocks.set(userName, predecessor.then(() => held));
4036
+ await predecessor;
4037
+ try {
4038
+ return await fn();
4039
+ } finally {
4040
+ release();
4041
+ }
4042
+ }
4043
+ /**
4044
+ * Delete the access key THIS attempt minted before losing its response
4045
+ * (issue [#2039](https://github.com/go-to-k/cdkd/issues/2039)).
4046
+ *
4047
+ * ## Why a reconcile rather than a token or an opt-out
4048
+ *
4049
+ * `CreateAccessKey` has no `ClientToken` / `ClientRequestToken` member, so
4050
+ * there is nothing to make the call itself idempotent. The other candidate
4051
+ * was `disableOuterRetry`, and it costs too much here: it would make the
4052
+ * provider single-shot for EVERY transient error, including the IAM
4053
+ * propagation window between a sibling `AWS::IAM::User` being created and
4054
+ * being visible to `CreateAccessKey`.
4055
+ *
4056
+ * The orphan cannot be ADOPTED — its secret was returned once, in the
4057
+ * response that was lost — so deleting it is the only remedy available.
4058
+ *
4059
+ * ## Three independent conditions, because deleting the wrong key is worse
4060
+ * than leaving an orphan
4061
+ *
4062
+ * A key is deleted only when ALL of these hold:
4063
+ *
4064
+ * 1. It is absent from the baseline taken moments earlier, under the lock.
4065
+ * 2. Its `CreateDate` is at or after this attempt started. IAM stamps that
4066
+ * date from ITS clock and the floor comes from ours, so a margin absorbs
4067
+ * ordinary skew; the residual risk is bounded by the length of ONE
4068
+ * `CreateAccessKey` call rather than by the retry schedule.
4069
+ * 3. cdkd did not create it successfully in this process. This is the one
4070
+ * the baseline structurally cannot express — "newer than my snapshot" is
4071
+ * equally true of my orphan and of somebody else's key.
4072
+ *
4073
+ * Anything that fails 2 or 3 is reported and LEFT ALONE. That is the safe
4074
+ * direction: an untracked key costs a quota slot and a manual cleanup, while
4075
+ * a wrongly deleted one destroys a live credential that cdkd state still
4076
+ * advertises — and because `readCurrentState` returns `undefined` for a
4077
+ * deleted key, `cdkd drift` would report UNKNOWN rather than drift, so the
4078
+ * loss would not even be visible.
4079
+ *
4080
+ * @param baseline `undefined` when the pre-create read failed, which disarms
4081
+ * the reconcile entirely — see {@link IAMAccessKeyProvider.tryListAccessKeyIds}.
4082
+ */
4083
+ async deleteOrphanFromFailedAttempt(userName, logicalId, baseline, attemptStartMs) {
4084
+ if (baseline === void 0) return;
4085
+ const current = await this.tryListAccessKeyMetadata(userName, logicalId);
4086
+ if (!current) return;
4087
+ for (const key of current) {
4088
+ const accessKeyId = key.accessKeyId;
4089
+ if (baseline.has(accessKeyId) || this.accessKeyIdsCreatedByThisProcess.has(accessKeyId)) continue;
4090
+ if (key.createDateMs === void 0 || key.createDateMs < attemptStartMs) {
4091
+ this.logger.warn(`IAM access key ${accessKeyId} appeared on user ${userName} while creating ${logicalId}, but cdkd cannot attribute it to this attempt (created ${key.createDateMs === void 0 ? "at an unknown time" : new Date(key.createDateMs).toISOString()}, attempt started ${new Date(attemptStartMs).toISOString()}). Leaving it in place. If it is an orphan of a failed cdkd deploy, delete it with: aws iam delete-access-key --user-name ${userName} --access-key-id ${accessKeyId}`);
4092
+ continue;
4093
+ }
4094
+ this.logger.warn(`IAM access key ${accessKeyId} was minted on user ${userName} by this failed attempt at ${logicalId} and its secret was lost with the response, so it is unusable and unrecorded; deleting it before the retry`);
4095
+ try {
4096
+ await this.iamClient.send(new DeleteAccessKeyCommand({
4097
+ UserName: userName,
4098
+ AccessKeyId: accessKeyId
4099
+ }));
4100
+ } catch (error) {
4101
+ this.logger.warn(`Failed to delete the orphaned IAM access key ${accessKeyId} for user ${userName}: ${error instanceof Error ? error.message : String(error)}. Manual deletion may be required: aws iam delete-access-key --user-name ${userName} --access-key-id ${accessKeyId}`);
4102
+ }
4103
+ }
4104
+ }
4105
+ /**
4106
+ * The user's current access key ids, or `undefined` when the read failed.
4107
+ *
4108
+ * `undefined` disarms the reconcile rather than failing the create: a missing
4109
+ * `iam:ListAccessKeys` permission must not turn a working deploy into a
4110
+ * broken one over a safety net.
4111
+ */
4112
+ async tryListAccessKeyIds(userName, logicalId) {
4113
+ const metadata = await this.tryListAccessKeyMetadata(userName, logicalId);
4114
+ return metadata && new Set(metadata.map((key) => key.accessKeyId));
4115
+ }
4116
+ /** `ListAccessKeys` with its `CreateDate`s, or `undefined` when the read failed. */
4117
+ async tryListAccessKeyMetadata(userName, logicalId) {
4118
+ const keys = [];
4119
+ let marker;
4120
+ try {
4121
+ do {
4122
+ const page = await this.iamClient.send(new ListAccessKeysCommand({
4123
+ UserName: userName,
4124
+ ...marker && { Marker: marker }
4125
+ }));
4126
+ for (const key of page.AccessKeyMetadata ?? []) if (key.AccessKeyId) keys.push({
4127
+ accessKeyId: key.AccessKeyId,
4128
+ createDateMs: key.CreateDate ? key.CreateDate.getTime() : void 0
4129
+ });
4130
+ marker = page.IsTruncated ? page.Marker : void 0;
4131
+ } while (marker);
4132
+ } catch (error) {
4133
+ this.logger.warn(`Could not list existing access keys for user ${userName} while creating ${logicalId}: ${error instanceof Error ? error.message : String(error)}. Orphan detection is DISABLED for this create, so a retried attempt may leave an unusable but ACTIVE access key on the user. Grant iam:ListAccessKeys, or check the user's keys after the deploy: aws iam list-access-keys --user-name ${userName}`);
4134
+ return;
4135
+ }
4136
+ return keys;
4137
+ }
4138
+ /**
3962
4139
  * Update an IAM access key.
3963
4140
  *
3964
4141
  * `UserName` / `Serial` are createOnly (replacement — this method never
@@ -23206,6 +23383,118 @@ var EventBridgeBusProvider = class {
23206
23383
  }
23207
23384
  };
23208
23385
 
23386
+ //#endregion
23387
+ //#region src/provisioning/providers/idempotency-token.ts
23388
+ /**
23389
+ * Retry-stable idempotency tokens for provider CREATE calls (issue
23390
+ * [#2039](https://github.com/go-to-k/cdkd/issues/2039)).
23391
+ *
23392
+ * ## The failure this exists to prevent
23393
+ *
23394
+ * The deploy engine wraps every `provider.create()` in an outer transient-error
23395
+ * retry (`src/deployment/retry.ts`), and issue #2026 made HTTP 500 / 502 / 504
23396
+ * retryable for the default classifier. A 500 whose request actually SUCCEEDED
23397
+ * server-side therefore re-invokes `create()` from the top. For a CREATE that
23398
+ * carries no name and no idempotency token, the replay mints a SECOND resource:
23399
+ * the first has no state entry, so `cdkd destroy` never reaches it and it bills
23400
+ * indefinitely. `RunInstances` is the worst instance of the class.
23401
+ *
23402
+ * ## What "stable" has to mean, precisely
23403
+ *
23404
+ * A token regenerated per attempt is not a fix — it is worse than none, because
23405
+ * the call site now LOOKS idempotent while behaving exactly as before. So the
23406
+ * value must be:
23407
+ *
23408
+ * - **Identical across every attempt of one logical create.** Guaranteed here
23409
+ * by memoising on `(scope, region, stackName, logicalId)` — a tuple the retry
23410
+ * loop reproduces byte-for-byte, because it re-invokes the same closure with
23411
+ * the same arguments. Deliberately NOT keyed on the resolved properties.
23412
+ * (The earlier version of this note justified that by saying the properties
23413
+ * are re-derived per attempt. They are NOT: `deploy-engine.ts` computes them
23414
+ * once OUTSIDE the retry closure. The reason to keep them out of the key is
23415
+ * that it makes the guarantee STRUCTURAL rather than contingent — an
23416
+ * identity built from a logical id cannot be perturbed by a future change to
23417
+ * how properties are resolved, whereas one built from their contents can.)
23418
+ * - **Different for a genuinely NEW create of the same logical resource.**
23419
+ * Reusing a token after a create SUCCEEDED is its own hazard: EC2 keeps a
23420
+ * `RunInstances` token for ~24h and answers a repeat with the ORIGINAL
23421
+ * instance, so a `--replace` re-create (delete old, then create with
23422
+ * unchanged properties, all in one process) would be handed back the
23423
+ * instance it had just terminated. {@link IdempotencyToken.release} is what
23424
+ * prevents that: it is called only on the success path, and it bumps a
23425
+ * per-key generation counter so the next acquire derives a fresh value.
23426
+ * - **Never reused by a LATER cdkd run.** A per-process nonce is mixed into
23427
+ * the digest, so two `cdkd deploy` invocations of the same template never
23428
+ * collide even though every other input is identical.
23429
+ *
23430
+ * ## Why not a purely deterministic hash of the create inputs
23431
+ *
23432
+ * That is what `EFSProvider`'s `CreationToken` and `FSxFileSystemProvider`'s
23433
+ * `ClientRequestToken` do, and it is right FOR THEM: those APIs enforce token
23434
+ * uniqueness only among LIVE file systems, so a re-deploy after a destroy is
23435
+ * unaffected, and hashing the immutable inputs is what lets the new file system
23436
+ * coexist with the old one during a replacement. EC2 is the opposite shape —
23437
+ * the token survives the resource's own termination — so a value that is stable
23438
+ * across RUNS is a liability there rather than an asset. Both providers keep
23439
+ * their existing derivation; this module is for the APIs where the token must
23440
+ * be scoped to one process and one create.
23441
+ */
23442
+ /**
23443
+ * Mixed into every digest so no two `cdkd` processes can derive the same token
23444
+ * from the same template. Evaluated once at module load; ESM guarantees a
23445
+ * single module instance per process, which is exactly the scope wanted.
23446
+ */
23447
+ const PROCESS_NONCE = randomUUID();
23448
+ /** Tokens currently in flight, keyed by {@link tokenKey}. */
23449
+ const inFlight = /* @__PURE__ */ new Map();
23450
+ /**
23451
+ * How many times each key has been RELEASED. Folded into the digest so a
23452
+ * post-success re-acquire (the `--replace` delete-then-create path) yields a
23453
+ * different token than the create that already succeeded.
23454
+ */
23455
+ const generations = /* @__PURE__ */ new Map();
23456
+ /**
23457
+ * Ceiling on {@link generations}. Chosen far above any realistic stack size so
23458
+ * eviction is unreachable in a normal `cdkd` run, and present only so a
23459
+ * long-lived process cannot grow the map without bound.
23460
+ */
23461
+ const MAX_TRACKED_GENERATIONS = 1e5;
23462
+ const tokenKey = (scope, logicalId) => `${scope}\u0000${process.env["AWS_REGION"] ?? ""}\u0000${getCurrentStackName() ?? ""}\u0000${logicalId}`;
23463
+ const derive = (key, generation, maxLength) => {
23464
+ return `cdkd-${createHash("sha256").update(`${PROCESS_NONCE}\u0000${key}\u0000${generation}`).digest("hex")}`.slice(0, maxLength);
23465
+ };
23466
+ /**
23467
+ * Get the idempotency token for one logical create, minting it on first use and
23468
+ * returning the SAME value for every retry of that create.
23469
+ *
23470
+ * @example
23471
+ * ```ts
23472
+ * const token = acquireIdempotencyToken({ scope: 'RunInstances', logicalId });
23473
+ * const response = await client.send(new RunInstancesCommand({ ...input, ClientToken: token.value }));
23474
+ * token.release(); // success path only
23475
+ * ```
23476
+ */
23477
+ function acquireIdempotencyToken(options) {
23478
+ const { scope, logicalId, maxLength = 64 } = options;
23479
+ const key = tokenKey(scope, logicalId);
23480
+ const existing = inFlight.get(key);
23481
+ const value = existing ?? derive(key, generations.get(key) ?? 0, maxLength);
23482
+ if (existing === void 0) inFlight.set(key, value);
23483
+ return {
23484
+ value,
23485
+ release: () => {
23486
+ if (!inFlight.has(key)) return;
23487
+ inFlight.delete(key);
23488
+ const generation = (generations.get(key) ?? 0) + 1;
23489
+ if (!generations.has(key) && generations.size >= MAX_TRACKED_GENERATIONS) {
23490
+ const oldest = generations.keys().next();
23491
+ if (!oldest.done) generations.delete(oldest.value);
23492
+ }
23493
+ generations.set(key, generation);
23494
+ }
23495
+ };
23496
+ }
23497
+
23209
23498
  //#endregion
23210
23499
  //#region src/utils/ip-protocol.ts
23211
23500
  /**
@@ -24274,8 +24563,13 @@ var EC2Provider = class {
24274
24563
  this.logger.debug(`Creating NatGateway ${logicalId}`);
24275
24564
  const subnetId = properties["SubnetId"];
24276
24565
  if (!subnetId) throw new ProvisioningError(`SubnetId is required for NatGateway ${logicalId}`, resourceType, logicalId);
24566
+ const natGatewayToken = acquireIdempotencyToken({
24567
+ scope: "CreateNatGateway",
24568
+ logicalId
24569
+ });
24277
24570
  try {
24278
24571
  const natGatewayId = (await this.ec2Client.send(new CreateNatGatewayCommand({
24572
+ ClientToken: natGatewayToken.value,
24279
24573
  SubnetId: subnetId,
24280
24574
  AllocationId: properties["AllocationId"],
24281
24575
  ConnectivityType: properties["ConnectivityType"] ?? void 0,
@@ -24296,6 +24590,7 @@ var EC2Provider = class {
24296
24590
  this.logger.debug(`NatGateway ${natGatewayId} is available`);
24297
24591
  } else this.logger.debug(`NatGateway ${natGatewayId} created (skipping available-state wait per --no-wait)`);
24298
24592
  this.logger.debug(`Successfully created NatGateway ${logicalId}: ${natGatewayId}`);
24593
+ natGatewayToken.release();
24299
24594
  return {
24300
24595
  physicalId: natGatewayId,
24301
24596
  attributes: { NatGatewayId: natGatewayId }
@@ -24338,10 +24633,18 @@ var EC2Provider = class {
24338
24633
  this.logger.debug(`Creating RouteTable ${logicalId}`);
24339
24634
  const vpcId = properties["VpcId"];
24340
24635
  if (!vpcId) throw new ProvisioningError(`VpcId is required for RouteTable ${logicalId}`, resourceType, logicalId);
24636
+ const routeTableToken = acquireIdempotencyToken({
24637
+ scope: "CreateRouteTable",
24638
+ logicalId
24639
+ });
24341
24640
  try {
24342
- const routeTableId = (await this.ec2Client.send(new CreateRouteTableCommand({ VpcId: vpcId }))).RouteTable.RouteTableId;
24641
+ const routeTableId = (await this.ec2Client.send(new CreateRouteTableCommand({
24642
+ VpcId: vpcId,
24643
+ ClientToken: routeTableToken.value
24644
+ }))).RouteTable.RouteTableId;
24343
24645
  await this.applyTags(routeTableId, properties, logicalId);
24344
24646
  this.logger.debug(`Successfully created RouteTable ${logicalId}: ${routeTableId}`);
24647
+ routeTableToken.release();
24345
24648
  return {
24346
24649
  physicalId: routeTableId,
24347
24650
  attributes: { RouteTableId: routeTableId }
@@ -24890,7 +25193,12 @@ var EC2Provider = class {
24890
25193
  const securityGroups = properties["SecurityGroups"];
24891
25194
  const iamInstanceProfile = this.normalizeIamInstanceProfile(properties["IamInstanceProfile"]);
24892
25195
  const networkInterfaces = this.buildNetworkInterfaces(properties);
25196
+ const runInstancesToken = acquireIdempotencyToken({
25197
+ scope: "RunInstances",
25198
+ logicalId
25199
+ });
24893
25200
  const instance = (await this.ec2Client.send(new RunInstancesCommand({
25201
+ ClientToken: runInstancesToken.value,
24894
25202
  ImageId: imageId,
24895
25203
  InstanceType: instanceType,
24896
25204
  KeyName: properties["KeyName"] ?? void 0,
@@ -24940,6 +25248,7 @@ var EC2Provider = class {
24940
25248
  AvailabilityZone: runningInstance?.Placement?.AvailabilityZone ?? ""
24941
25249
  };
24942
25250
  this.logger.debug(`Successfully created EC2 Instance ${logicalId}: ${instanceId}`);
25251
+ runInstancesToken.release();
24943
25252
  return {
24944
25253
  physicalId: instanceId,
24945
25254
  attributes
@@ -24947,6 +25256,7 @@ var EC2Provider = class {
24947
25256
  } catch (innerError) {
24948
25257
  try {
24949
25258
  await this.ec2Client.send(new TerminateInstancesCommand({ InstanceIds: [instanceId] }));
25259
+ runInstancesToken.release();
24950
25260
  this.logger.debug(`Terminate requested for partially-created EC2 Instance ${logicalId} (${instanceId}) after wiring failure (not waiting for terminated state)`);
24951
25261
  } catch (cleanupError) {
24952
25262
  this.logger.warn(`Failed to terminate partially-created EC2 Instance ${logicalId} (${instanceId}): ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}. THE INSTANCE IS STILL RUNNING AND BILLING. Manual termination required: aws ec2 terminate-instances --instance-ids ${instanceId}`);
@@ -25461,10 +25771,18 @@ var EC2Provider = class {
25461
25771
  this.logger.debug(`Creating NetworkAcl ${logicalId}`);
25462
25772
  const vpcId = properties["VpcId"];
25463
25773
  if (!vpcId) throw new ProvisioningError(`VpcId is required for NetworkAcl ${logicalId}`, resourceType, logicalId);
25774
+ const networkAclToken = acquireIdempotencyToken({
25775
+ scope: "CreateNetworkAcl",
25776
+ logicalId
25777
+ });
25464
25778
  try {
25465
- const networkAclId = (await this.ec2Client.send(new CreateNetworkAclCommand({ VpcId: vpcId }))).NetworkAcl.NetworkAclId;
25779
+ const networkAclId = (await this.ec2Client.send(new CreateNetworkAclCommand({
25780
+ VpcId: vpcId,
25781
+ ClientToken: networkAclToken.value
25782
+ }))).NetworkAcl.NetworkAclId;
25466
25783
  await this.applyTags(networkAclId, properties, logicalId);
25467
25784
  this.logger.debug(`Successfully created NetworkAcl ${logicalId}: ${networkAclId}`);
25785
+ networkAclToken.release();
25468
25786
  return {
25469
25787
  physicalId: networkAclId,
25470
25788
  attributes: { Id: networkAclId }
@@ -38735,9 +39053,14 @@ var Route53Provider = class {
38735
39053
  const createComment = readConfigString(hostedZoneConfig, "Comment", "", "AWS::Route53::HostedZone HostedZoneConfig");
38736
39054
  const vpcs = properties["VPCs"];
38737
39055
  const firstVpc = vpcs && vpcs.length > 0 ? vpcs[0] : void 0;
38738
- const response = await this.getClient().send(new CreateHostedZoneCommand({
39056
+ const callerReferenceToken = acquireIdempotencyToken({
39057
+ scope: "CreateHostedZone",
39058
+ logicalId,
39059
+ maxLength: 128
39060
+ });
39061
+ const response = await this.createOrAdoptHostedZone({
38739
39062
  Name: name,
38740
- CallerReference: `${logicalId}-${Date.now()}`,
39063
+ CallerReference: callerReferenceToken.value,
38741
39064
  ...createComment !== "" ? { HostedZoneConfig: {
38742
39065
  Comment: createComment,
38743
39066
  ...firstVpc ? { PrivateZone: true } : {}
@@ -38746,7 +39069,7 @@ var Route53Provider = class {
38746
39069
  VPCId: firstVpc["VPCId"],
38747
39070
  VPCRegion: firstVpc["VPCRegion"]
38748
39071
  } } : {}
38749
- }));
39072
+ });
38750
39073
  const hostedZone = response.HostedZone;
38751
39074
  if (!hostedZone?.Id) throw new Error("CreateHostedZone did not return HostedZone.Id");
38752
39075
  const zoneId = hostedZone.Id.replace("/hostedzone/", "");
@@ -38773,6 +39096,7 @@ var Route53Provider = class {
38773
39096
  try {
38774
39097
  await this.deleteQueryLoggingConfigForZone(zoneId, logicalId);
38775
39098
  await this.getClient().send(new DeleteHostedZoneCommand({ Id: zoneId }));
39099
+ callerReferenceToken.release();
38776
39100
  } catch (deleteError) {
38777
39101
  this.logger.warn(`Best-effort rollback DeleteHostedZone for ${zoneId} also failed: ${deleteError instanceof Error ? deleteError.message : String(deleteError)} — operator may need to clean up the orphan zone`);
38778
39102
  }
@@ -38781,6 +39105,7 @@ var Route53Provider = class {
38781
39105
  }
38782
39106
  const nameServers = response.DelegationSet?.NameServers ?? [];
38783
39107
  this.logger.debug(`Successfully created hosted zone ${logicalId}: ${zoneId}`);
39108
+ callerReferenceToken.release();
38784
39109
  return {
38785
39110
  physicalId: zoneId,
38786
39111
  attributes: {
@@ -38794,6 +39119,77 @@ var Route53Provider = class {
38794
39119
  throw new ProvisioningError(`Failed to create hosted zone ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
38795
39120
  }
38796
39121
  }
39122
+ /**
39123
+ * `CreateHostedZone`, with the retry-replay case ADOPTED rather than failed
39124
+ * (issue [#2039](https://github.com/go-to-k/cdkd/issues/2039)).
39125
+ *
39126
+ * A stable `CallerReference` stops a replayed create from minting a second
39127
+ * zone, but on its own it converts the replay into a hard
39128
+ * `HostedZoneAlreadyExists` — so the first attempt's zone would still be live
39129
+ * and still absent from state, only now the deploy fails as well. Route 53
39130
+ * reports the caller reference back on every `HostedZone`, which is what lets
39131
+ * the replay find the zone the lost response described and continue with it.
39132
+ *
39133
+ * The lookup is `ListHostedZonesByName`, which starts at the requested DNS
39134
+ * name and returns zones in name order, so the walk stops as soon as the name
39135
+ * changes rather than paging the whole account. A `GetHostedZone` follows,
39136
+ * because only that call returns the `DelegationSet` whose `NameServers` are
39137
+ * the resource's `Fn::GetAtt` value — an adopted zone must return the same
39138
+ * attributes a first-attempt create would have.
39139
+ *
39140
+ * Finding nothing rethrows the original error: a caller reference AWS says is
39141
+ * taken but that names no zone we can see is not something to paper over.
39142
+ */
39143
+ async createOrAdoptHostedZone(input) {
39144
+ try {
39145
+ return await this.getClient().send(new CreateHostedZoneCommand(input));
39146
+ } catch (error) {
39147
+ if (!(error instanceof HostedZoneAlreadyExists || error?.name === "HostedZoneAlreadyExists")) throw error;
39148
+ let adopted;
39149
+ try {
39150
+ adopted = await this.findHostedZoneByCallerReference(input.Name, input.CallerReference);
39151
+ } catch (lookupError) {
39152
+ this.logger.warn(`CreateHostedZone for ${input.Name} was refused as already-existing, and the lookup that would have adopted the existing zone failed: ${lookupError instanceof Error ? lookupError.message : String(lookupError)}. Reporting the original error.`);
39153
+ throw error;
39154
+ }
39155
+ if (!adopted?.Id) throw error;
39156
+ this.logger.warn(`CreateHostedZone for ${input.Name} was replayed after a lost response; adopting the hosted zone ${adopted.Id} the previous attempt already created instead of creating a second one`);
39157
+ try {
39158
+ const described = await this.getClient().send(new GetHostedZoneCommand({ Id: adopted.Id }));
39159
+ return {
39160
+ HostedZone: described.HostedZone ?? adopted,
39161
+ ...described.DelegationSet ? { DelegationSet: described.DelegationSet } : {}
39162
+ };
39163
+ } catch (describeError) {
39164
+ this.logger.warn(`Adopted hosted zone ${adopted.Id} but could not read it back: ${describeError instanceof Error ? describeError.message : String(describeError)}. Reporting the original error rather than returning a zone with no NameServers.`);
39165
+ throw error;
39166
+ }
39167
+ }
39168
+ }
39169
+ /**
39170
+ * The hosted zone carrying `callerReference`, or `undefined`.
39171
+ *
39172
+ * Scoped to zones whose name matches `dnsName`: `ListHostedZonesByName` is
39173
+ * ordered by name, so once the name differs no later page can hold ours.
39174
+ */
39175
+ async findHostedZoneByCallerReference(dnsName, callerReference) {
39176
+ const wanted = canonicalizeQueryName(dnsName);
39177
+ let marker = { DNSName: wanted };
39178
+ while (marker) {
39179
+ const page = await this.getClient().send(new ListHostedZonesByNameCommand({
39180
+ DNSName: marker.DNSName,
39181
+ ...marker.HostedZoneId ? { HostedZoneId: marker.HostedZoneId } : {}
39182
+ }));
39183
+ for (const zone of page.HostedZones ?? []) {
39184
+ if (canonicalizeQueryName(zone.Name ?? "") !== wanted) return;
39185
+ if (zone.CallerReference === callerReference) return zone;
39186
+ }
39187
+ marker = page.IsTruncated && page.NextDNSName ? {
39188
+ DNSName: page.NextDNSName,
39189
+ ...page.NextHostedZoneId ? { HostedZoneId: page.NextHostedZoneId } : {}
39190
+ } : void 0;
39191
+ }
39192
+ }
38797
39193
  async updateHostedZone(logicalId, physicalId, resourceType, properties, previousProperties) {
38798
39194
  this.logger.debug(`Updating Route 53 hosted zone ${logicalId}: ${physicalId}`);
38799
39195
  try {
@@ -83294,7 +83690,7 @@ function createMigrateCommand() {
83294
83690
  */
83295
83691
  function buildProgram() {
83296
83692
  const program = new Command();
83297
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.16");
83693
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.17");
83298
83694
  program.hook("preAction", (_thisCommand, actionCommand) => {
83299
83695
  const { profile } = actionCommand.optsWithGlobals();
83300
83696
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;