@go-to-k/cdkd 0.284.16 → 0.284.18

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-HK-oROJf.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-IPF2MfAC.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 }
@@ -34435,10 +34753,18 @@ var ELBv2Provider = class {
34435
34753
  * `withRetry`, whose per-attempt `debug` line and give-up `warn` summary
34436
34754
  * interpolate the AWS message verbatim — and that payload is built from
34437
34755
  * RESOLVED template properties. See {@link maskedRetryLogger}.
34756
+ *
34757
+ * The LoadBalancer arm receives it too (issue #2063). It runs no `withRetry`,
34758
+ * but it has the two surfaces that do not need one: the `ProvisioningError`
34759
+ * its catch throws (printed at ERROR — DEFAULT verbosity — by
34760
+ * `deploy-engine.ts`) and its partial-create cleanup `warn`. Its payload is
34761
+ * `Name` / `Subnets` / `SecurityGroups` / `LoadBalancerAttributes` values /
34762
+ * `Tags` values straight out of the resolved bag, and AWS quotes a rejected
34763
+ * value back.
34438
34764
  */
34439
34765
  async create(logicalId, resourceType, properties, context) {
34440
34766
  switch (resourceType) {
34441
- case "AWS::ElasticLoadBalancingV2::LoadBalancer": return this.createLoadBalancer(logicalId, resourceType, properties);
34767
+ case "AWS::ElasticLoadBalancingV2::LoadBalancer": return this.createLoadBalancer(logicalId, resourceType, properties, context?.maskSecrets);
34442
34768
  case "AWS::ElasticLoadBalancingV2::TargetGroup": return this.createTargetGroup(logicalId, resourceType, properties, context?.maskSecrets);
34443
34769
  case "AWS::ElasticLoadBalancingV2::Listener": return this.createListener(logicalId, resourceType, properties, context?.maskSecrets);
34444
34770
  default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId);
@@ -34447,6 +34773,15 @@ var ELBv2Provider = class {
34447
34773
  /**
34448
34774
  * `context` is read for ONE thing today: `maskSecrets` (issue #2050) — the
34449
34775
  * update-path twin of the `create()` note above.
34776
+ *
34777
+ * The catch below is the LoadBalancer arm's masking site (issue #2063), and
34778
+ * the reason that arm needs no `maskSecrets` parameter of its own.
34779
+ * `updateLoadBalancer` has no try/catch: its `ModifyLoadBalancerAttributes` /
34780
+ * `SetSubnets` / `SetSecurityGroups` / `AddTags` rejections — every one of
34781
+ * them built from the resolved `properties` bag — propagate RAW to here.
34782
+ * The sibling arms wrap their own errors into a `ProvisioningError` first,
34783
+ * so the `CdkdError` passthrough short-circuits them and this frame masks
34784
+ * only what actually escapes unwrapped.
34450
34785
  */
34451
34786
  async update(logicalId, physicalId, resourceType, properties, previousProperties, context) {
34452
34787
  try {
@@ -34454,12 +34789,12 @@ var ELBv2Provider = class {
34454
34789
  } catch (error) {
34455
34790
  if (error instanceof CdkdError) throw error;
34456
34791
  const cause = error instanceof Error ? error : void 0;
34457
- throw new ProvisioningError(`Failed to update ELBv2 resource ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
34792
+ throw new ProvisioningError(`Failed to update ELBv2 resource ${logicalId}: ${this.maskErrorMessage(error, context?.maskSecrets)}`, resourceType, logicalId, physicalId, cause);
34458
34793
  }
34459
34794
  }
34460
34795
  async applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties, context) {
34461
34796
  switch (resourceType) {
34462
- case "AWS::ElasticLoadBalancingV2::LoadBalancer": return this.updateLoadBalancer(logicalId, physicalId, resourceType, properties, previousProperties);
34797
+ case "AWS::ElasticLoadBalancingV2::LoadBalancer": return this.updateLoadBalancer(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
34463
34798
  case "AWS::ElasticLoadBalancingV2::TargetGroup": return this.updateTargetGroup(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
34464
34799
  case "AWS::ElasticLoadBalancingV2::Listener": return this.updateListener(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
34465
34800
  default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId, physicalId);
@@ -34473,7 +34808,7 @@ var ELBv2Provider = class {
34473
34808
  default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId, physicalId);
34474
34809
  }
34475
34810
  }
34476
- async createLoadBalancer(logicalId, resourceType, properties) {
34811
+ async createLoadBalancer(logicalId, resourceType, properties, maskSecrets) {
34477
34812
  this.logger.debug(`Creating LoadBalancer ${logicalId}`);
34478
34813
  try {
34479
34814
  const tags = this.extractTags(properties);
@@ -34520,7 +34855,7 @@ var ELBv2Provider = class {
34520
34855
  SecurityGroups: properties["SecurityGroups"] ?? [],
34521
34856
  EnforceSecurityGroupInboundRulesOnPrivateLinkTraffic: enforcePrivateLink
34522
34857
  }));
34523
- this.logger.debug(`Applied EnforceSecurityGroupInboundRulesOnPrivateLinkTraffic=${enforcePrivateLink} for ${logicalId}`);
34858
+ this.logger.debug(`Applied EnforceSecurityGroupInboundRulesOnPrivateLinkTraffic=${maskerOrIdentity(maskSecrets)(String(enforcePrivateLink))} for ${logicalId}`);
34524
34859
  }
34525
34860
  const minCapacity = properties["MinimumLoadBalancerCapacity"];
34526
34861
  if (minCapacity?.CapacityUnits !== void 0) {
@@ -34528,7 +34863,7 @@ var ELBv2Provider = class {
34528
34863
  LoadBalancerArn: lbArn,
34529
34864
  MinimumLoadBalancerCapacity: { CapacityUnits: Number(minCapacity.CapacityUnits) }
34530
34865
  }));
34531
- this.logger.debug(`Requested capacity reservation of ${minCapacity.CapacityUnits} LCU for ${logicalId}`);
34866
+ this.logger.debug(`Requested capacity reservation of ${maskerOrIdentity(maskSecrets)(String(minCapacity.CapacityUnits))} LCU for ${logicalId}`);
34532
34867
  if (isTruthyCfnBoolean(properties["EnableCapacityReservationProvisionStabilize"]) && process.env["CDKD_NO_WAIT"] !== "true") await this.waitForCapacityReservationProvisioned(lbArn, logicalId);
34533
34868
  }
34534
34869
  } catch (innerError) {
@@ -34536,7 +34871,7 @@ var ELBv2Provider = class {
34536
34871
  await this.getClient().send(new DeleteLoadBalancerCommand({ LoadBalancerArn: lbArn }));
34537
34872
  this.logger.debug(`Cleaned up partially-created LoadBalancer ${logicalId} (${lbArn}) after wiring failure`);
34538
34873
  } catch (cleanupError) {
34539
- this.logger.warn(`Failed to clean up partially-created LoadBalancer ${logicalId} (${lbArn}): ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}. Manual deletion may be required before the next deploy: aws elbv2 delete-load-balancer --load-balancer-arn ${lbArn}`);
34874
+ this.logger.warn(`Failed to clean up partially-created LoadBalancer ${logicalId} (${lbArn}): ${this.maskErrorMessage(cleanupError, maskSecrets)}. Manual deletion may be required before the next deploy: aws elbv2 delete-load-balancer --load-balancer-arn ${lbArn}`);
34540
34875
  }
34541
34876
  throw innerError;
34542
34877
  }
@@ -34552,10 +34887,10 @@ var ELBv2Provider = class {
34552
34887
  };
34553
34888
  } catch (error) {
34554
34889
  const cause = error instanceof Error ? error : void 0;
34555
- throw new ProvisioningError(`Failed to create LoadBalancer ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
34890
+ throw new ProvisioningError(`Failed to create LoadBalancer ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, void 0, cause);
34556
34891
  }
34557
34892
  }
34558
- async updateLoadBalancer(logicalId, physicalId, _resourceType, properties, previousProperties) {
34893
+ async updateLoadBalancer(logicalId, physicalId, _resourceType, properties, previousProperties, maskSecrets) {
34559
34894
  const handledKeys = /* @__PURE__ */ new Set([
34560
34895
  "LoadBalancerAttributes",
34561
34896
  "Subnets",
@@ -34637,7 +34972,7 @@ var ELBv2Provider = class {
34637
34972
  LoadBalancerArn: physicalId,
34638
34973
  ...newCapacityUnits !== void 0 ? { MinimumLoadBalancerCapacity: { CapacityUnits: newCapacityUnits } } : { ResetCapacityReservation: true }
34639
34974
  }));
34640
- this.logger.debug(newCapacityUnits !== void 0 ? `Requested capacity reservation of ${newCapacityUnits} LCU for ${logicalId}` : `Reset capacity reservation for ${logicalId}`);
34975
+ this.logger.debug(newCapacityUnits !== void 0 ? `Requested capacity reservation of ${maskerOrIdentity(maskSecrets)(String(newCapacity?.CapacityUnits))} LCU for ${logicalId}` : `Reset capacity reservation for ${logicalId}`);
34641
34976
  if (newCapacityUnits !== void 0 && isTruthyCfnBoolean(properties["EnableCapacityReservationProvisionStabilize"]) && process.env["CDKD_NO_WAIT"] !== "true") await this.waitForCapacityReservationProvisioned(physicalId, logicalId);
34642
34977
  }
34643
34978
  await this.applyTagDiff(physicalId, previousProperties["Tags"], properties["Tags"]);
@@ -34655,6 +34990,16 @@ var ELBv2Provider = class {
34655
34990
  * a timeout WARNS and continues (the reservation keeps provisioning
34656
34991
  * asynchronously and the ModifyCapacityReservation was accepted), while a
34657
34992
  * `failed` zonal state throws — that reservation will never provision.
34993
+ *
34994
+ * Takes NO masker, deliberately (issue #2063 audit). Its only request is
34995
+ * `DescribeCapacityReservation({ LoadBalancerArn })` with an AWS-issued ARN,
34996
+ * so no resolved template value is in the payload for AWS to quote back into
34997
+ * the transient-failure `debug` line; the timeout `warn` and the `failed`
34998
+ * throw are assembled from a logical id, a count and an AWS-supplied zonal
34999
+ * reason. A structural reason, not a likelihood judgement — add the
35000
+ * parameter if this ever sends a template-derived member. The `failed` throw
35001
+ * is an ordinary `Error`, so it lands in `createLoadBalancer`'s masked catch
35002
+ * on the create path and in `update()`'s on the update path either way.
34658
35003
  */
34659
35004
  async waitForCapacityReservationProvisioned(lbArn, logicalId) {
34660
35005
  const maxAttempts = 40;
@@ -38735,9 +39080,14 @@ var Route53Provider = class {
38735
39080
  const createComment = readConfigString(hostedZoneConfig, "Comment", "", "AWS::Route53::HostedZone HostedZoneConfig");
38736
39081
  const vpcs = properties["VPCs"];
38737
39082
  const firstVpc = vpcs && vpcs.length > 0 ? vpcs[0] : void 0;
38738
- const response = await this.getClient().send(new CreateHostedZoneCommand({
39083
+ const callerReferenceToken = acquireIdempotencyToken({
39084
+ scope: "CreateHostedZone",
39085
+ logicalId,
39086
+ maxLength: 128
39087
+ });
39088
+ const response = await this.createOrAdoptHostedZone({
38739
39089
  Name: name,
38740
- CallerReference: `${logicalId}-${Date.now()}`,
39090
+ CallerReference: callerReferenceToken.value,
38741
39091
  ...createComment !== "" ? { HostedZoneConfig: {
38742
39092
  Comment: createComment,
38743
39093
  ...firstVpc ? { PrivateZone: true } : {}
@@ -38746,7 +39096,7 @@ var Route53Provider = class {
38746
39096
  VPCId: firstVpc["VPCId"],
38747
39097
  VPCRegion: firstVpc["VPCRegion"]
38748
39098
  } } : {}
38749
- }));
39099
+ });
38750
39100
  const hostedZone = response.HostedZone;
38751
39101
  if (!hostedZone?.Id) throw new Error("CreateHostedZone did not return HostedZone.Id");
38752
39102
  const zoneId = hostedZone.Id.replace("/hostedzone/", "");
@@ -38773,6 +39123,7 @@ var Route53Provider = class {
38773
39123
  try {
38774
39124
  await this.deleteQueryLoggingConfigForZone(zoneId, logicalId);
38775
39125
  await this.getClient().send(new DeleteHostedZoneCommand({ Id: zoneId }));
39126
+ callerReferenceToken.release();
38776
39127
  } catch (deleteError) {
38777
39128
  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
39129
  }
@@ -38781,6 +39132,7 @@ var Route53Provider = class {
38781
39132
  }
38782
39133
  const nameServers = response.DelegationSet?.NameServers ?? [];
38783
39134
  this.logger.debug(`Successfully created hosted zone ${logicalId}: ${zoneId}`);
39135
+ callerReferenceToken.release();
38784
39136
  return {
38785
39137
  physicalId: zoneId,
38786
39138
  attributes: {
@@ -38794,6 +39146,77 @@ var Route53Provider = class {
38794
39146
  throw new ProvisioningError(`Failed to create hosted zone ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
38795
39147
  }
38796
39148
  }
39149
+ /**
39150
+ * `CreateHostedZone`, with the retry-replay case ADOPTED rather than failed
39151
+ * (issue [#2039](https://github.com/go-to-k/cdkd/issues/2039)).
39152
+ *
39153
+ * A stable `CallerReference` stops a replayed create from minting a second
39154
+ * zone, but on its own it converts the replay into a hard
39155
+ * `HostedZoneAlreadyExists` — so the first attempt's zone would still be live
39156
+ * and still absent from state, only now the deploy fails as well. Route 53
39157
+ * reports the caller reference back on every `HostedZone`, which is what lets
39158
+ * the replay find the zone the lost response described and continue with it.
39159
+ *
39160
+ * The lookup is `ListHostedZonesByName`, which starts at the requested DNS
39161
+ * name and returns zones in name order, so the walk stops as soon as the name
39162
+ * changes rather than paging the whole account. A `GetHostedZone` follows,
39163
+ * because only that call returns the `DelegationSet` whose `NameServers` are
39164
+ * the resource's `Fn::GetAtt` value — an adopted zone must return the same
39165
+ * attributes a first-attempt create would have.
39166
+ *
39167
+ * Finding nothing rethrows the original error: a caller reference AWS says is
39168
+ * taken but that names no zone we can see is not something to paper over.
39169
+ */
39170
+ async createOrAdoptHostedZone(input) {
39171
+ try {
39172
+ return await this.getClient().send(new CreateHostedZoneCommand(input));
39173
+ } catch (error) {
39174
+ if (!(error instanceof HostedZoneAlreadyExists || error?.name === "HostedZoneAlreadyExists")) throw error;
39175
+ let adopted;
39176
+ try {
39177
+ adopted = await this.findHostedZoneByCallerReference(input.Name, input.CallerReference);
39178
+ } catch (lookupError) {
39179
+ 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.`);
39180
+ throw error;
39181
+ }
39182
+ if (!adopted?.Id) throw error;
39183
+ 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`);
39184
+ try {
39185
+ const described = await this.getClient().send(new GetHostedZoneCommand({ Id: adopted.Id }));
39186
+ return {
39187
+ HostedZone: described.HostedZone ?? adopted,
39188
+ ...described.DelegationSet ? { DelegationSet: described.DelegationSet } : {}
39189
+ };
39190
+ } catch (describeError) {
39191
+ 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.`);
39192
+ throw error;
39193
+ }
39194
+ }
39195
+ }
39196
+ /**
39197
+ * The hosted zone carrying `callerReference`, or `undefined`.
39198
+ *
39199
+ * Scoped to zones whose name matches `dnsName`: `ListHostedZonesByName` is
39200
+ * ordered by name, so once the name differs no later page can hold ours.
39201
+ */
39202
+ async findHostedZoneByCallerReference(dnsName, callerReference) {
39203
+ const wanted = canonicalizeQueryName(dnsName);
39204
+ let marker = { DNSName: wanted };
39205
+ while (marker) {
39206
+ const page = await this.getClient().send(new ListHostedZonesByNameCommand({
39207
+ DNSName: marker.DNSName,
39208
+ ...marker.HostedZoneId ? { HostedZoneId: marker.HostedZoneId } : {}
39209
+ }));
39210
+ for (const zone of page.HostedZones ?? []) {
39211
+ if (canonicalizeQueryName(zone.Name ?? "") !== wanted) return;
39212
+ if (zone.CallerReference === callerReference) return zone;
39213
+ }
39214
+ marker = page.IsTruncated && page.NextDNSName ? {
39215
+ DNSName: page.NextDNSName,
39216
+ ...page.NextHostedZoneId ? { HostedZoneId: page.NextHostedZoneId } : {}
39217
+ } : void 0;
39218
+ }
39219
+ }
38797
39220
  async updateHostedZone(logicalId, physicalId, resourceType, properties, previousProperties) {
38798
39221
  this.logger.debug(`Updating Route 53 hosted zone ${logicalId}: ${physicalId}`);
38799
39222
  try {
@@ -41818,25 +42241,38 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
41818
42241
  * `context` is read for ONE thing today: `maskSecrets` (issue #2050). The
41819
42242
  * Service create path runs a post-create `UpdateServiceAttributes` through
41820
42243
  * `withRetry` — see {@link maskedRetryLogger}.
42244
+ *
42245
+ * EVERY arm receives it, not just the Service one (issue #2063). The three
42246
+ * namespace arms build their `Create*NamespaceCommand` payload out of the
42247
+ * RESOLVED `properties` bag — `Name`, `Description`, `Tags` — and each has
42248
+ * two disclosure surfaces for it: the `ProvisioningError` its catch throws
42249
+ * (printed at ERROR, i.e. DEFAULT verbosity, by `deploy-engine.ts`) and the
42250
+ * AWS `ErrorMessage` that {@link pollOperation} raises for a FAILED
42251
+ * operation. Cloud Map's create is operation-based, so the second one is the
42252
+ * NORMAL rejection path for those arms, not an edge case.
41821
42253
  */
41822
42254
  async create(logicalId, resourceType, properties, context) {
41823
42255
  switch (resourceType) {
41824
- case "AWS::ServiceDiscovery::PrivateDnsNamespace": return this.createNamespace(logicalId, resourceType, properties);
41825
- case "AWS::ServiceDiscovery::HttpNamespace": return this.createHttpNamespace(logicalId, resourceType, properties);
41826
- case "AWS::ServiceDiscovery::PublicDnsNamespace": return this.createPublicDnsNamespace(logicalId, resourceType, properties);
42256
+ case "AWS::ServiceDiscovery::PrivateDnsNamespace": return this.createNamespace(logicalId, resourceType, properties, context?.maskSecrets);
42257
+ case "AWS::ServiceDiscovery::HttpNamespace": return this.createHttpNamespace(logicalId, resourceType, properties, context?.maskSecrets);
42258
+ case "AWS::ServiceDiscovery::PublicDnsNamespace": return this.createPublicDnsNamespace(logicalId, resourceType, properties, context?.maskSecrets);
41827
42259
  case "AWS::ServiceDiscovery::Service": return this.createService(logicalId, resourceType, properties, context?.maskSecrets);
41828
42260
  default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId);
41829
42261
  }
41830
42262
  }
41831
42263
  /**
41832
42264
  * `context` is read for ONE thing today: `maskSecrets` (issue #2050) — the
41833
- * update-path twin of the `create()` note above.
42265
+ * update-path twin of the `create()` note above, and threaded into every arm
42266
+ * for the same reason (issue #2063). The namespace update arms carry one
42267
+ * payload surface the create arms do not: {@link syncNamespaceTags} sends
42268
+ * `TagResource` with the resolved `Tags` VALUES, whose rejection lands in
42269
+ * the arm's own catch.
41834
42270
  */
41835
42271
  update(logicalId, physicalId, resourceType, properties, previousProperties, context) {
41836
42272
  switch (resourceType) {
41837
- case "AWS::ServiceDiscovery::PrivateDnsNamespace": return this.updateNamespace(logicalId, physicalId, resourceType, properties, previousProperties);
41838
- case "AWS::ServiceDiscovery::HttpNamespace": return this.updateHttpNamespace(logicalId, physicalId, resourceType, properties, previousProperties);
41839
- case "AWS::ServiceDiscovery::PublicDnsNamespace": return this.updatePublicDnsNamespace(logicalId, physicalId, resourceType, properties, previousProperties);
42273
+ case "AWS::ServiceDiscovery::PrivateDnsNamespace": return this.updateNamespace(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
42274
+ case "AWS::ServiceDiscovery::HttpNamespace": return this.updateHttpNamespace(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
42275
+ case "AWS::ServiceDiscovery::PublicDnsNamespace": return this.updatePublicDnsNamespace(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
41840
42276
  case "AWS::ServiceDiscovery::Service": return this.updateService(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
41841
42277
  default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId, physicalId);
41842
42278
  }
@@ -41850,7 +42286,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
41850
42286
  default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId, physicalId);
41851
42287
  }
41852
42288
  }
41853
- async createNamespace(logicalId, resourceType, properties) {
42289
+ async createNamespace(logicalId, resourceType, properties, maskSecrets) {
41854
42290
  this.logger.debug(`Creating private DNS namespace ${logicalId}`);
41855
42291
  const client = this.getClient();
41856
42292
  const name = properties["Name"];
@@ -41869,7 +42305,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
41869
42305
  ...inputProperties && { Properties: inputProperties }
41870
42306
  }))).OperationId;
41871
42307
  if (!operationId) throw new Error("CreatePrivateDnsNamespace did not return OperationId");
41872
- const namespaceId = await this.pollOperation(operationId, logicalId, resourceType);
42308
+ const namespaceId = await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
41873
42309
  const arn = await this.buildNamespaceArn(namespaceId);
41874
42310
  this.logger.debug(`Successfully created private DNS namespace ${logicalId}: ${namespaceId}`);
41875
42311
  return {
@@ -41882,7 +42318,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
41882
42318
  } catch (error) {
41883
42319
  if (error instanceof ProvisioningError) throw error;
41884
42320
  const cause = error instanceof Error ? error : void 0;
41885
- throw new ProvisioningError(`Failed to create private DNS namespace ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
42321
+ throw new ProvisioningError(`Failed to create private DNS namespace ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, void 0, cause);
41886
42322
  }
41887
42323
  }
41888
42324
  /**
@@ -41908,7 +42344,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
41908
42344
  * kind's Cloud Map default ({@link PRIVATE_DNS_NAMESPACE_DEFAULT_SOA_TTL}) —
41909
42345
  * the CFn-parity shape, live A/B'd 2026-08-11 on the private kind itself.
41910
42346
  */
41911
- async updateNamespace(logicalId, physicalId, resourceType, properties, previousProperties) {
42347
+ async updateNamespace(logicalId, physicalId, resourceType, properties, previousProperties, maskSecrets) {
41912
42348
  this.logger.debug(`Updating private DNS namespace ${logicalId}: ${physicalId}`);
41913
42349
  const client = this.getClient();
41914
42350
  const namespaceChange = {};
@@ -41922,7 +42358,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
41922
42358
  Id: physicalId,
41923
42359
  Namespace: namespaceChange
41924
42360
  }))).OperationId;
41925
- if (operationId) await this.pollOperation(operationId, logicalId, resourceType);
42361
+ if (operationId) await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
41926
42362
  } else this.logger.debug(`No mutable namespace-body diff for PrivateDnsNamespace ${logicalId}`);
41927
42363
  await this.syncNamespaceTags(logicalId, physicalId, properties, previousProperties);
41928
42364
  this.logger.debug(`Successfully updated private DNS namespace ${logicalId}`);
@@ -41933,7 +42369,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
41933
42369
  } catch (error) {
41934
42370
  if (error instanceof ProvisioningError) throw error;
41935
42371
  const cause = error instanceof Error ? error : void 0;
41936
- throw new ProvisioningError(`Failed to update private DNS namespace ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
42372
+ throw new ProvisioningError(`Failed to update private DNS namespace ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, physicalId, cause);
41937
42373
  }
41938
42374
  }
41939
42375
  /**
@@ -41967,7 +42403,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
41967
42403
  throw new ProvisioningError(`Failed to delete Cloud Map namespace ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
41968
42404
  }
41969
42405
  }
41970
- async createHttpNamespace(logicalId, resourceType, properties) {
42406
+ async createHttpNamespace(logicalId, resourceType, properties, maskSecrets) {
41971
42407
  this.logger.debug(`Creating HTTP namespace ${logicalId}`);
41972
42408
  const client = this.getClient();
41973
42409
  const name = properties["Name"];
@@ -41981,7 +42417,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
41981
42417
  ...tags && tags.length > 0 && { Tags: tags }
41982
42418
  }))).OperationId;
41983
42419
  if (!operationId) throw new Error("CreateHttpNamespace did not return OperationId");
41984
- const namespaceId = await this.pollOperation(operationId, logicalId, resourceType);
42420
+ const namespaceId = await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
41985
42421
  const arn = await this.resolveNamespaceArn(namespaceId);
41986
42422
  this.logger.debug(`Successfully created HTTP namespace ${logicalId}: ${namespaceId}`);
41987
42423
  return {
@@ -41994,7 +42430,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
41994
42430
  } catch (error) {
41995
42431
  if (error instanceof ProvisioningError) throw error;
41996
42432
  const cause = error instanceof Error ? error : void 0;
41997
- throw new ProvisioningError(`Failed to create HTTP namespace ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
42433
+ throw new ProvisioningError(`Failed to create HTTP namespace ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, void 0, cause);
41998
42434
  }
41999
42435
  }
42000
42436
  /**
@@ -42015,7 +42451,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42015
42451
  * the template must be cleared explicitly via the `''` sentinel — the
42016
42452
  * CFn-parity shape, live A/B'd 2026-08-11 on this exact kind.
42017
42453
  */
42018
- async updateHttpNamespace(logicalId, physicalId, resourceType, properties, previousProperties) {
42454
+ async updateHttpNamespace(logicalId, physicalId, resourceType, properties, previousProperties, maskSecrets) {
42019
42455
  this.logger.debug(`Updating HTTP namespace ${logicalId}: ${physicalId}`);
42020
42456
  const client = this.getClient();
42021
42457
  const description = clearOnUpdateRemoval(properties["Description"], previousProperties["Description"], "");
@@ -42026,7 +42462,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42026
42462
  Id: physicalId,
42027
42463
  Namespace: namespaceChange
42028
42464
  }))).OperationId;
42029
- if (operationId) await this.pollOperation(operationId, logicalId, resourceType);
42465
+ if (operationId) await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
42030
42466
  }
42031
42467
  await this.syncNamespaceTags(logicalId, physicalId, properties, previousProperties);
42032
42468
  this.logger.debug(`Successfully updated HTTP namespace ${logicalId}`);
@@ -42037,10 +42473,10 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42037
42473
  } catch (error) {
42038
42474
  if (error instanceof ProvisioningError) throw error;
42039
42475
  const cause = error instanceof Error ? error : void 0;
42040
- throw new ProvisioningError(`Failed to update HTTP namespace ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
42476
+ throw new ProvisioningError(`Failed to update HTTP namespace ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, physicalId, cause);
42041
42477
  }
42042
42478
  }
42043
- async createPublicDnsNamespace(logicalId, resourceType, properties) {
42479
+ async createPublicDnsNamespace(logicalId, resourceType, properties, maskSecrets) {
42044
42480
  this.logger.debug(`Creating public DNS namespace ${logicalId}`);
42045
42481
  const client = this.getClient();
42046
42482
  const name = properties["Name"];
@@ -42056,7 +42492,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42056
42492
  ...inputProperties && { Properties: inputProperties }
42057
42493
  }))).OperationId;
42058
42494
  if (!operationId) throw new Error("CreatePublicDnsNamespace did not return OperationId");
42059
- const namespaceId = await this.pollOperation(operationId, logicalId, resourceType);
42495
+ const namespaceId = await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
42060
42496
  let arn;
42061
42497
  let hostedZoneId;
42062
42498
  try {
@@ -42079,7 +42515,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42079
42515
  } catch (error) {
42080
42516
  if (error instanceof ProvisioningError) throw error;
42081
42517
  const cause = error instanceof Error ? error : void 0;
42082
- throw new ProvisioningError(`Failed to create public DNS namespace ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
42518
+ throw new ProvisioningError(`Failed to create public DNS namespace ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, void 0, cause);
42083
42519
  }
42084
42520
  }
42085
42521
  /**
@@ -42097,7 +42533,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42097
42533
  * kind's Cloud Map default ({@link PUBLIC_DNS_NAMESPACE_DEFAULT_SOA_TTL}) —
42098
42534
  * the CFn-parity shape, live A/B'd 2026-08-11 on this exact kind.
42099
42535
  */
42100
- async updatePublicDnsNamespace(logicalId, physicalId, resourceType, properties, previousProperties) {
42536
+ async updatePublicDnsNamespace(logicalId, physicalId, resourceType, properties, previousProperties, maskSecrets) {
42101
42537
  this.logger.debug(`Updating public DNS namespace ${logicalId}: ${physicalId}`);
42102
42538
  const client = this.getClient();
42103
42539
  const namespaceChange = {};
@@ -42111,7 +42547,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42111
42547
  Id: physicalId,
42112
42548
  Namespace: namespaceChange
42113
42549
  }))).OperationId;
42114
- if (operationId) await this.pollOperation(operationId, logicalId, resourceType);
42550
+ if (operationId) await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
42115
42551
  }
42116
42552
  await this.syncNamespaceTags(logicalId, physicalId, properties, previousProperties);
42117
42553
  this.logger.debug(`Successfully updated public DNS namespace ${logicalId}`);
@@ -42122,7 +42558,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42122
42558
  } catch (error) {
42123
42559
  if (error instanceof ProvisioningError) throw error;
42124
42560
  const cause = error instanceof Error ? error : void 0;
42125
- throw new ProvisioningError(`Failed to update public DNS namespace ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
42561
+ throw new ProvisioningError(`Failed to update public DNS namespace ${logicalId}: ${this.maskErrorMessage(error, maskSecrets)}`, resourceType, logicalId, physicalId, cause);
42126
42562
  }
42127
42563
  }
42128
42564
  async createService(logicalId, resourceType, properties, maskSecrets) {
@@ -42228,7 +42664,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42228
42664
  Id: physicalId,
42229
42665
  Service: serviceChange
42230
42666
  }))).OperationId;
42231
- if (operationId) await this.pollOperation(operationId, logicalId, resourceType);
42667
+ if (operationId) await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
42232
42668
  }
42233
42669
  if (hasAttrUpsert) {
42234
42670
  await withRetry(() => client.send(new UpdateServiceAttributesCommand({
@@ -42299,6 +42735,15 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42299
42735
  * Resolve a namespace's ARN — authoritative via `GetNamespace`, with a
42300
42736
  * deterministic STS-based construction as fallback so a transient read
42301
42737
  * failure right after create does not fail the whole resource.
42738
+ *
42739
+ * Takes NO masker, deliberately (issue #2063 audit). Its request is
42740
+ * `GetNamespace({ Id })` with an AWS-issued namespace id — no resolved
42741
+ * template value is in the payload, so the AWS error text it logs cannot
42742
+ * echo one back. That is a structural reason, not an assessment of
42743
+ * likelihood; add the parameter if this call ever grows a template-derived
42744
+ * member. The `TagResource` / `UntagResource` calls in
42745
+ * {@link syncNamespaceTags} DO carry resolved values, but they throw rather
42746
+ * than log, and the throw lands in the calling arm's masked catch.
42302
42747
  */
42303
42748
  async resolveNamespaceArn(namespaceId) {
42304
42749
  try {
@@ -42341,8 +42786,39 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42341
42786
  /**
42342
42787
  * Poll a Service Discovery operation until it completes.
42343
42788
  * Returns the target resource ID from the operation result.
42344
- */
42345
- async pollOperation(operationId, logicalId, resourceType) {
42789
+ *
42790
+ * MASKS `Operation.ErrorMessage` (issue #2063), and this is the only place
42791
+ * that can. Cloud Map's create / update are operation-based: the caller's
42792
+ * `Create*Namespace` / `Update*` request carried the RESOLVED `properties`
42793
+ * bag — `Name`, `Description`, `Tags`, `DnsConfig` — and AWS reports its
42794
+ * rejection here rather than by rejecting the submit call, quoting the
42795
+ * offending value back in `ErrorMessage`. A FAILED operation is therefore
42796
+ * the NORMAL rejection path for these arms, not an edge case.
42797
+ *
42798
+ * Why not leave it to the arms' own catch: every namespace arm and
42799
+ * `updateService` open with `if (error instanceof ProvisioningError) throw
42800
+ * error;`, so a `ProvisioningError` raised here is re-thrown VERBATIM and
42801
+ * never reaches their `maskErrorMessage`. `deploy-engine.ts` then prints it
42802
+ * at ERROR — DEFAULT verbosity. That passthrough is correct (it preserves
42803
+ * the resource/logical-id context this frame added), which is exactly why
42804
+ * the mask has to be applied at the point of construction.
42805
+ *
42806
+ * `errorMessage` is masked RAW, before any interpolation. Stating precisely
42807
+ * what that buys, because the obvious phrasing over-claims (issue #2063
42808
+ * review): `maskSecretsInText`'s WHOLE-VALUE arm fires only when
42809
+ * `secrets.has(text)` — i.e. when the text IS the secret in its entirety —
42810
+ * so masking raw covers the case where AWS returns the offending value as
42811
+ * the whole `ErrorMessage`, at ANY length, including below the substring
42812
+ * arm's 4-character `MIN_NEEDLE_LENGTH` floor. When AWS instead quotes the
42813
+ * value INSIDE a sentence, the substring arm handles it and that floor
42814
+ * applies either way, raw or not. So masking here is strictly better than
42815
+ * masking the assembled message, but it is not general sub-4-character
42816
+ * coverage. See `SecretMaskingContext` in `src/types/resource.ts`.
42817
+ *
42818
+ * The `operationId` / timeout message below carries no template-derived
42819
+ * value (an AWS-issued opaque id), so it is left as-is.
42820
+ */
42821
+ async pollOperation(operationId, logicalId, resourceType, maskSecrets) {
42346
42822
  const client = this.getClient();
42347
42823
  const maxAttempts = 60;
42348
42824
  let delay = 1e3;
@@ -42354,7 +42830,7 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42354
42830
  if (targets) return targets["NAMESPACE"] || targets["SERVICE"] || operationId;
42355
42831
  return operationId;
42356
42832
  }
42357
- if (status === "FAIL") throw new ProvisioningError(`Operation failed for ${logicalId}: ${result.Operation?.ErrorMessage || "Unknown error"}`, resourceType, logicalId);
42833
+ if (status === "FAIL") throw new ProvisioningError(`Operation failed for ${logicalId}: ${maskerOrIdentity(maskSecrets)(result.Operation?.ErrorMessage || "Unknown error")}`, resourceType, logicalId);
42358
42834
  await new Promise((resolve) => setTimeout(resolve, delay));
42359
42835
  delay = Math.min(delay * 2, 1e4);
42360
42836
  }
@@ -83294,7 +83770,7 @@ function createMigrateCommand() {
83294
83770
  */
83295
83771
  function buildProgram() {
83296
83772
  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");
83773
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.18");
83298
83774
  program.hook("preAction", (_thisCommand, actionCommand) => {
83299
83775
  const { profile } = actionCommand.optsWithGlobals();
83300
83776
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;