@go-to-k/cdkd 0.284.39 → 0.284.41

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 startInterruptWatch, $n as CdkdError, $t as WorkGraph, A as renderStatefulReason, An as resolveStateBucketWithDefault, At as redactSecretsForState, B as exportAliasCollisionScrubWarning, Bn as expectedOwnerParam, Bt as DiffCalculator, C as isFinalSnapshotError, Cn as synthesisStatusMessage, Cr as isMarkedNonRetryable, Ct as TEMPLATE_SOURCED_RULES, D as extractDeploymentEventError, Dn as resolveAutoAssetStorage, Dt as isSingleDynamicReferenceToken, E as makeCanonicalizePropertiesFn, En as resolveApp, Er as markNonRetryable, Et as errorCauseChain, F as green, Fn as CFN_TEMPLATE_BODY_LIMIT, Ft as s3BucketDomainName, G as collectInlinePolicyNamesManagedBySiblings, Gn as processStackMessages, Gt as TemplateParser, H as secretBearingStateKeyWarning, Hn as canonicalizeRegion, Ht as describeTypeWithThrottleRetry, I as red, In as CFN_TEMPLATE_URL_LIMIT, It as s3BucketDualStackDomainName, J as findActionableSilentDrops, Jn as AwsClients, Jt as S3StateBackend, K as clearOnUpdateRemoval, Kt as LockManager, L as yellow, Ln as MIGRATE_TMP_PREFIX, Lt as s3BucketRegionalDomainName, M as bold, Mn as resolveUseCdkBootstrapAssets, Mt as classifyReplaySecretRegion, N as cyan, Nn as stateBucketExistenceConfirmed, Nt as producerRegionsFromState, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as resolveCaptureObservedState, Ot as maskSecretsInError, P as gray, Pn as warnDeprecatedNoPrefixCliFlag, Pt as s3BucketArn, Q as isInterruptedWaitError, Qt as stringifyValue, R as collectDeclaredOutputNames, Rn as findLargeInlineResources, Rt as s3BucketWebsiteUrl, S as createPreDeleteFinalSnapshot, Sn as Synthesizer, Sr as withErrorHandling, St as STATE_SOURCED_READBACK_RULES, T as unsupportedFinalSnapshotError, Tn as getLegacyStateBucketName, Tr as isThrottlingError, Tt as dynamicReferenceTokens, U as stateKeySecretExposure, Un as derivePartitionAndUrlSuffix, Ut as withRetry, V as isExportAliasCollision, Vn as PARTITION_TABLE, Vt as INTRINSIC_KEYS, W as IAMRoleProvider, Wn as AssemblyReader, Wt as DagBuilder, X as beginCommandInterruptScope, Xn as resetAwsClients, Xt as shouldRetainResource, Y as findSilentDropProperties, Yn as getAwsClients, Yt as rebuildClientForBucketRegion, Z as endCommandInterruptScope, Zn as setAwsClients, Zt as AssetPublisher, _ as computeImplicitDeleteEdges, _n as getDockerCmd, _t as replayWarn, a as DeploymentEventsStore, an as stripControlChars, ar as LocalInvokeBuildError$1, at as carriesDynamicReference, b as buildFinalSnapshotIdentifier, bn as AssetManifestLoader, bt as requireConfigString, c as replayFailedOperations, cn as ensureAssetStorage, ct as refStateLookupFromResource, d as updatePartialReason, dn as readBootstrapMarkerBody, dr as PartialFailureError, dt as resolveExplicitPhysicalId, en as buildAssetRedirectMap, et as CloudControlProvider, f as UNSPECIFIED_SKIP_REASON, fn as validateAssetBucketName, fr as ProvisioningError, ft as assertRegionMatch, g as IMPLICIT_DELETE_DEPENDENCIES, gn as formatDockerLoginError, gr as StackTerminationProtectionError, gt as readConfigString, h as maskingRetryLogger, hn as buildDockerImage, hr as StackHasActiveImportsError, ht as configStringRefusal, i as DeploymentEventsReader, in as escapeRegExp, ir as DynamicReferenceRegionAmbiguousError, it as IntrinsicFunctionResolver, j as formatResourceLine, jn as resolveStateBucketWithDefaultAndSource, jt as scrubResourceRecord, k as isStatefulRecreateTargetSync, kn as resolveSkipPrefix, kt as maskSecretsInText, l as replayRollback, ln as getBootstrapMarkerKey, lr as MissingCdkCliError, lt as WAFv2WebACLProvider, m as withResourceDeadline, mn as buildDenyExternalAccessPolicy, mr as ResourceUpdateNotSupportedError, mt as configBooleanRefusal, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as loadPublishableAssetManifest, nt as disableInstanceApiTermination, o as planFailedOps, on as AssetModeResolver, or as LocalMigrateError, ot as cfnRefValueFromPhysicalId, p as deleteSkipReason, pn as validateContainerRepoName, pr as ResourceTimeoutError, pt as coerceCfnBoolean, q as ProviderRegistry, qn as resolveBucketRegion, qt as displaySafe, r as DeployEngine, rn as rewriteTemplateAssetReferences, rr as DeployCancelledError, rt as isTerminationProtectionPropagationError, s as planRollback, sn as BOOTSTRAP_MARKER_PREFIX, sr as LocalStartServiceError, st as getAccountInfo, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as createAssetRedirectResolver, tr as CrossAccountSecretRefusalError, tt as slowCcOperationTimeoutMs, u as updatePartialMessage, un as parseBootstrapMarker, ur as NestedStackChildDirectDestroyError, ut as normalizeAwsTagsToCfn, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as runDockerForeground, vt as requireConfigArray, w as refusesFinalSnapshot, wn as getDefaultStateBucketName, wr as isRetryableTransientError, wt as createSecretMasker, x as ccRoutedFinalSnapshotError, xn as getDockerImageBySourceHash, xr as normalizeAwsError, xt as STATE_SOURCED_CROSS_GENERATION_RULES, y as PRE_DELETE_SNAPSHOT_TYPES, yn as runDockerStreaming, yt as requireConfigObject, z as collectPublishedOutputNames, zn as uploadCfnTemplate, zt as applyRoleArnIfSet } from "./deploy-engine-rKBFVifY.js";
2
+ import { $ as startInterruptWatch, $n as CdkdError, $t as WorkGraph, A as renderStatefulReason, An as resolveStateBucketWithDefault, At as redactSecretsForState, B as exportAliasCollisionScrubWarning, Bn as expectedOwnerParam, Bt as DiffCalculator, C as isFinalSnapshotError, Cn as synthesisStatusMessage, Cr as isMarkedNonRetryable, Ct as TEMPLATE_SOURCED_RULES, D as extractDeploymentEventError, Dn as resolveAutoAssetStorage, Dt as isSingleDynamicReferenceToken, E as makeCanonicalizePropertiesFn, En as resolveApp, Er as markNonRetryable, Et as errorCauseChain, F as green, Fn as CFN_TEMPLATE_BODY_LIMIT, Ft as s3BucketDomainName, G as collectInlinePolicyNamesManagedBySiblings, Gn as processStackMessages, Gt as TemplateParser, H as secretBearingStateKeyWarning, Hn as canonicalizeRegion, Ht as describeTypeWithThrottleRetry, I as red, In as CFN_TEMPLATE_URL_LIMIT, It as s3BucketDualStackDomainName, J as findActionableSilentDrops, Jn as AwsClients, Jt as S3StateBackend, K as clearOnUpdateRemoval, Kt as LockManager, L as yellow, Ln as MIGRATE_TMP_PREFIX, Lt as s3BucketRegionalDomainName, M as bold, Mn as resolveUseCdkBootstrapAssets, Mt as classifyReplaySecretRegion, N as cyan, Nn as stateBucketExistenceConfirmed, Nt as producerRegionsFromState, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as resolveCaptureObservedState, Ot as maskSecretsInError, P as gray, Pn as warnDeprecatedNoPrefixCliFlag, Pt as s3BucketArn, Q as isInterruptedWaitError, Qt as stringifyValue, R as collectDeclaredOutputNames, Rn as findLargeInlineResources, Rt as s3BucketWebsiteUrl, S as createPreDeleteFinalSnapshot, Sn as Synthesizer, Sr as withErrorHandling, St as STATE_SOURCED_READBACK_RULES, T as unsupportedFinalSnapshotError, Tn as getLegacyStateBucketName, Tr as isThrottlingError, Tt as dynamicReferenceTokens, U as stateKeySecretExposure, Un as derivePartitionAndUrlSuffix, Ut as withRetry, V as isExportAliasCollision, Vn as PARTITION_TABLE, Vt as INTRINSIC_KEYS, W as IAMRoleProvider, Wn as AssemblyReader, Wt as DagBuilder, X as beginCommandInterruptScope, Xn as resetAwsClients, Xt as shouldRetainResource, Y as findSilentDropProperties, Yn as getAwsClients, Yt as rebuildClientForBucketRegion, Z as endCommandInterruptScope, Zn as setAwsClients, Zt as AssetPublisher, _ as computeImplicitDeleteEdges, _n as getDockerCmd, _t as replayWarn, a as DeploymentEventsStore, an as stripControlChars, ar as LocalInvokeBuildError$1, at as carriesDynamicReference, b as buildFinalSnapshotIdentifier, bn as AssetManifestLoader, bt as requireConfigString, c as replayFailedOperations, cn as ensureAssetStorage, ct as refStateLookupFromResource, d as updatePartialReason, dn as readBootstrapMarkerBody, dr as PartialFailureError, dt as resolveExplicitPhysicalId, en as buildAssetRedirectMap, et as CloudControlProvider, f as UNSPECIFIED_SKIP_REASON, fn as validateAssetBucketName, fr as ProvisioningError, ft as assertRegionMatch, g as IMPLICIT_DELETE_DEPENDENCIES, gn as formatDockerLoginError, gr as StackTerminationProtectionError, gt as readConfigString, h as maskingRetryLogger, hn as buildDockerImage, hr as StackHasActiveImportsError, ht as configStringRefusal, i as DeploymentEventsReader, in as escapeRegExp, ir as DynamicReferenceRegionAmbiguousError, it as IntrinsicFunctionResolver, j as formatResourceLine, jn as resolveStateBucketWithDefaultAndSource, jt as scrubResourceRecord, k as isStatefulRecreateTargetSync, kn as resolveSkipPrefix, kt as maskSecretsInText, l as replayRollback, ln as getBootstrapMarkerKey, lr as MissingCdkCliError, lt as WAFv2WebACLProvider, m as withResourceDeadline, mn as buildDenyExternalAccessPolicy, mr as ResourceUpdateNotSupportedError, mt as configBooleanRefusal, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as loadPublishableAssetManifest, nt as disableInstanceApiTermination, o as planFailedOps, on as AssetModeResolver, or as LocalMigrateError, ot as cfnRefValueFromPhysicalId, p as deleteSkipReason, pn as validateContainerRepoName, pr as ResourceTimeoutError, pt as coerceCfnBoolean, q as ProviderRegistry, qn as resolveBucketRegion, qt as displaySafe, r as DeployEngine, rn as rewriteTemplateAssetReferences, rr as DeployCancelledError, rt as isTerminationProtectionPropagationError, s as planRollback, sn as BOOTSTRAP_MARKER_PREFIX, sr as LocalStartServiceError, st as getAccountInfo, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as createAssetRedirectResolver, tr as CrossAccountSecretRefusalError, tt as slowCcOperationTimeoutMs, u as updatePartialMessage, un as parseBootstrapMarker, ur as NestedStackChildDirectDestroyError, ut as normalizeAwsTagsToCfn, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as runDockerForeground, vt as requireConfigArray, w as refusesFinalSnapshot, wn as getDefaultStateBucketName, wr as isRetryableTransientError, wt as createSecretMasker, x as ccRoutedFinalSnapshotError, xn as getDockerImageBySourceHash, xr as normalizeAwsError, xt as STATE_SOURCED_CROSS_GENERATION_RULES, y as PRE_DELETE_SNAPSHOT_TYPES, yn as runDockerStreaming, yt as requireConfigObject, z as collectPublishedOutputNames, zn as uploadCfnTemplate, zt as applyRoleArnIfSet } from "./deploy-engine-CCWl_tAH.js";
3
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-Db6lnsaA.js";
4
+ import { t as ASGProvider } from "./asg-provider-D0lenV1M.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";
@@ -10324,6 +10324,121 @@ function isSqsQueueUrl(value) {
10324
10324
  return value.startsWith("https://sqs.") && value.includes("/");
10325
10325
  }
10326
10326
 
10327
+ //#endregion
10328
+ //#region src/provisioning/masked-retry-logger.ts
10329
+ /**
10330
+ * The masker a caller supplied, or the identity function when it supplied
10331
+ * none.
10332
+ *
10333
+ * ABSENT MEANS UNMASKED, and that is the back-compatible default the contract
10334
+ * mandates — `create()` / `update()` are also reached from the import path,
10335
+ * from `cdkd drift --revert`, and from tests, and a provider must not care
10336
+ * which caller it got. Centralised here so no call site re-spells the `??`
10337
+ * and accidentally makes the capability required.
10338
+ */
10339
+ function maskerOrIdentity(maskSecrets) {
10340
+ return maskSecrets ?? ((text) => text);
10341
+ }
10342
+ /**
10343
+ * A {@link RetryLogger} whose every line is routed through `maskSecrets`.
10344
+ *
10345
+ * `warn` is ALWAYS provided, never omitted. It is optional on `RetryLogger`,
10346
+ * and dropping it would silence the give-up summary on the calling path only —
10347
+ * trading a disclosure for the reporting hole issue #2018 closed. It goes
10348
+ * through the SAME mask as `debug` precisely because it is the line that
10349
+ * survives a run without `--verbose`; forwarding it unmasked would defeat the
10350
+ * fence at a HIGHER log level than the one the fence was written for.
10351
+ *
10352
+ * `logger` is typed structurally rather than as the concrete `Logger` so this
10353
+ * module stays a leaf and a test can pass two spies. That matches the
10354
+ * precedent already in the tree (`buildMfaConfigRequest` in
10355
+ * `cognito-provider.ts` takes an injected `logger?: { warn }`).
10356
+ */
10357
+ function createMaskedRetryLogger(logger, maskSecrets) {
10358
+ const mask = maskerOrIdentity(maskSecrets);
10359
+ return {
10360
+ debug: (message) => logger.debug(mask(message)),
10361
+ warn: (message) => logger.warn(mask(message))
10362
+ };
10363
+ }
10364
+ /**
10365
+ * Depth cap for {@link maskDeep}.
10366
+ *
10367
+ * What it actually bounds is unbounded WORK on a pathologically deep bag — see
10368
+ * the rationale at the cap itself, which is the authority. It is NOT primarily
10369
+ * a cycle guard: a template-derived bag cannot be cyclic, so that justification
10370
+ * over-claims (issue #2176 security review). It does still terminate one, which
10371
+ * is why the walk is safe to share with a caller that might hand it something
10372
+ * other than a template bag.
10373
+ *
10374
+ * Deliberately generous. Every shape these warnings exist to describe is a
10375
+ * scalar, a list of scalars, or a small record, so the walk does not reach
10376
+ * depth 2 in practice.
10377
+ */
10378
+ const MASK_WALK_MAX_DEPTH = 8;
10379
+ /**
10380
+ * What {@link maskDeep} substitutes for a subtree it declines to descend into.
10381
+ *
10382
+ * MUST equal `SECRET_MASK` in `src/deployment/secret-redaction.ts`. It is
10383
+ * spelled here rather than imported so this module keeps its single-import leaf
10384
+ * property (see the module note above); the two are fenced against drift by a
10385
+ * test that imports both.
10386
+ */
10387
+ const MASK_WALK_DEPTH_CAP_MARKER = "***";
10388
+ /**
10389
+ * Mask every string LEAF and KEY of an arbitrary value, returning a structure
10390
+ * safe to `JSON.stringify` into a log line or an error message (issue
10391
+ * [#2176](https://github.com/go-to-k/cdkd/issues/2176)).
10392
+ *
10393
+ * THE ORDERING IS THE WHOLE POINT, and it is why masking the finished message
10394
+ * is not a substitute. `maskSecretsInText` matches by literal occurrence, so:
10395
+ *
10396
+ * 1. ESCAPING. `JSON.stringify` escapes `"`, `\` and newlines, so a secret
10397
+ * containing any of them no longer OCCURS in the stringified text and a
10398
+ * mask applied afterwards cannot find it. That is not an exotic case — it
10399
+ * is every Secrets Manager JSON document, the commonest real secret shape.
10400
+ * Measured on the pre-fix tree for #2176: a plaintext of
10401
+ * `{"user":"admin","pw":"hunter2"}` interpolated as
10402
+ * `${JSON.stringify(value)}` came through a message-level mask COMPLETELY
10403
+ * unchanged, while masking the leaves first rendered it `"***"`.
10404
+ * 2. LENGTH. A finished message is always longer than the value inside it, so
10405
+ * it can only ever reach `maskSecretsInText`'s SUBSTRING arm, which ignores
10406
+ * needles below `MIN_NEEDLE_LENGTH` (4). Handing the masker each RAW leaf
10407
+ * reaches the WHOLE-VALUE arm, which has no floor. Measured the same way: a
10408
+ * 3-character secret survived `Value 'abc' at 'pin' failed ...` intact.
10409
+ *
10410
+ * Neither pass subsumes the other, so providers do BOTH — this walk on the
10411
+ * value, and the assembled message through the masker as well. The masker is
10412
+ * idempotent, so the overlap is free.
10413
+ *
10414
+ * Keys are masked as well as values: `JSON.stringify` renders them into the
10415
+ * same line, so a resolved secret used as a map key would otherwise escape.
10416
+ *
10417
+ * WHY THIS LIVES HERE rather than as a private walk per provider. SIX
10418
+ * hand-rolled copies had already accumulated — `elbv2-provider.ts`,
10419
+ * `cognito-provider.ts`, `sns-topic-provider.ts`, `dynamodb-table-provider.ts`,
10420
+ * `dynamodb-globaltable-provider.ts` and `apigatewayv2-provider.ts` — and
10421
+ * `elbv2-provider.ts` carried the standing instruction that "a THIRD site is
10422
+ * the point at which this should move into `../masked-retry-logger.ts` ... and
10423
+ * all three converge on it". That trigger had fired and been missed twice over,
10424
+ * and the copies had already diverged exactly as predicted: FOUR of the six
10425
+ * carried NO depth cap at all.
10426
+ *
10427
+ * Worth recording how the last two were nearly missed AGAIN, because it is the
10428
+ * same failure one level up: the first sweep for this issue grepped for
10429
+ * `maskDeep` / `MASK_WALK_MAX_DEPTH` — the SPELLINGS the known copies used —
10430
+ * and the two survivors spell it `maskLeaf` / `maskLeafValue` with no named
10431
+ * constant. Grep for the SHAPE, not the name. These copies encode a security
10432
+ * contract, and a hardening applied to one silently leaves the others behind.
10433
+ */
10434
+ function maskDeep(value, mask, depth = 0) {
10435
+ if (typeof value === "string") return mask(value);
10436
+ if (depth >= 8) return "***";
10437
+ if (Array.isArray(value)) return value.map((entry) => maskDeep(entry, mask, depth + 1));
10438
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [mask(k), maskDeep(v, mask, depth + 1)]));
10439
+ return value;
10440
+ }
10441
+
10327
10442
  //#endregion
10328
10443
  //#region src/provisioning/providers/sns-topic-provider.ts
10329
10444
  /**
@@ -10979,12 +11094,7 @@ function buildDeliveryStatusAttributeMap(logging, logicalId, onMalformed, maskSe
10979
11094
  const warn = (message) => {
10980
11095
  getLogger().child("SNSTopicProvider").warn(maskSecrets(message));
10981
11096
  };
10982
- const maskLeaf = (value) => {
10983
- if (typeof value === "string") return maskSecrets(value);
10984
- if (Array.isArray(value)) return value.map(maskLeaf);
10985
- if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [maskSecrets(k), maskLeaf(v)]));
10986
- return value;
10987
- };
11097
+ const maskLeaf = (value) => maskDeep(value, maskSecrets);
10988
11098
  const seenProtocols = /* @__PURE__ */ new Set();
10989
11099
  if (!Array.isArray(logging)) {
10990
11100
  if (onMalformed === "throw") throw new Error(`SNS topic ${logicalId}: DeliveryStatusLogging must be an array of {Protocol, ...} objects, got ${JSON.stringify(maskLeaf(logging))}`);
@@ -13955,10 +14065,11 @@ function idShape(segments) {
13955
14065
  * mis-names one value frequently mis-names its sibling too, and reporting them
13956
14066
  * one deploy at a time is the shape of remedy nobody finishes.
13957
14067
  */
13958
- function compositeIdSeparatorRefusal(resourceType, logicalId, segments) {
14068
+ function compositeIdSeparatorRefusal(resourceType, logicalId, segments, maskSecrets) {
13959
14069
  const offending = segments.filter((segment) => String(segment.value).includes("|"));
13960
14070
  if (offending.length === 0) return void 0;
13961
- return `${resourceType} ${logicalId}: ${offending.map((segment) => `${segment.name} '${String(segment.value)}'`).join(" and ")} contains '${"|"}', which cdkd uses as the separator in this type's physical id (${idShape(segments)}). Recording it would produce an id that decodes back to a DIFFERENT resource, so a later cdkd destroy / drift / update would target the wrong one — or silently skip it while the real resource stays alive. AWS accepts the name and CloudFormation manages such a resource fine; this is a cdkd limitation tracked in ${COMPOSITE_ID_ISSUE_URL}. Rename the resource so no segment contains '${"|"}', or manage it outside cdkd.`;
14071
+ const mask = maskerOrIdentity(maskSecrets);
14072
+ return `${resourceType} ${logicalId}: ${offending.map((segment) => `${segment.name} '${mask(String(segment.value))}'`).join(" and ")} contains '${"|"}', which cdkd uses as the separator in this type's physical id (${idShape(segments)}). Recording it would produce an id that decodes back to a DIFFERENT resource, so a later cdkd destroy / drift / update would target the wrong one — or silently skip it while the real resource stays alive. AWS accepts the name and CloudFormation manages such a resource fine; this is a cdkd limitation tracked in ${COMPOSITE_ID_ISSUE_URL}. Rename the resource so no segment contains '${"|"}', or manage it outside cdkd.`;
13962
14073
  }
13963
14074
  /**
13964
14075
  * Join the segments into a composite physicalId, REFUSING one that would be
@@ -13970,7 +14081,7 @@ function compositeIdSeparatorRefusal(resourceType, logicalId, segments) {
13970
14081
  * behavior.
13971
14082
  */
13972
14083
  function packCompositeId(resourceType, logicalId, segments, options) {
13973
- const refusal = compositeIdSeparatorRefusal(resourceType, logicalId, segments);
14084
+ const refusal = compositeIdSeparatorRefusal(resourceType, logicalId, segments, options?.maskSecrets);
13974
14085
  if (refusal !== void 0) if (options?.onRefusal) options.onRefusal(`${refusal} Continuing with the ambiguous id because the value comes from a cdkd state record, which no template edit can repair; the same value is REFUSED on a template-path create.`);
13975
14086
  else throw new ProvisioningError(refusal, resourceType, logicalId);
13976
14087
  return segments.map((segment) => String(segment.value)).join("|");
@@ -14163,7 +14274,10 @@ var LambdaEventInvokeConfigProvider = class {
14163
14274
  coerceNumber: true,
14164
14275
  ...replayWarn(this.logger, context)
14165
14276
  });
14166
- const physicalId = this.buildPhysicalId(logicalId, functionName, qualifier, context?.replayingState === true ? { onRefusal: (message) => this.logger.warn(message) } : void 0);
14277
+ const physicalId = this.buildPhysicalId(logicalId, functionName, qualifier, {
14278
+ maskSecrets: context?.maskSecrets,
14279
+ ...context?.replayingState === true && { onRefusal: (message) => this.logger.warn(message) }
14280
+ });
14167
14281
  try {
14168
14282
  await this.lambdaClient.send(new PutFunctionEventInvokeConfigCommand(this.buildPutInput(properties)));
14169
14283
  this.logger.debug(`Successfully created Lambda EventInvokeConfig ${logicalId}: ${physicalId}`);
@@ -16333,10 +16447,7 @@ const deleteTableRetryDelays$1 = {};
16333
16447
  * leaf is stringified — and escaped — identically.
16334
16448
  */
16335
16449
  function maskLeafValue$1(value, maskSecrets) {
16336
- if (typeof value === "string") return maskSecrets(value);
16337
- if (Array.isArray(value)) return value.map((v) => maskLeafValue$1(v, maskSecrets));
16338
- if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [maskSecrets(k), maskLeafValue$1(v, maskSecrets)]));
16339
- return value;
16450
+ return maskDeep(value, maskSecrets);
16340
16451
  }
16341
16452
  var DynamoDBTableProvider = class {
16342
16453
  dynamoDBClient;
@@ -17245,7 +17356,7 @@ var DynamoDBTableProvider = class {
17245
17356
  const warn = (message) => this.logger.warn(maskSecrets(message));
17246
17357
  const coerced = coerceWarmThroughput(value);
17247
17358
  if (coerced === void 0) return void 0;
17248
- if (coerced.dropped.length > 0) warn(`${scope}: WarmThroughput member(s) ${coerced.dropped.join(", ")} in ${JSON.stringify(maskLeafValue$1(value, maskSecrets))} are not a number DynamoDB accepts, so they were dropped from the request, which leaves ${JSON.stringify(coerced.spec)}. Check for an unresolved intrinsic or a non-numeric value.`);
17359
+ if (coerced.dropped.length > 0) warn(`${scope}: WarmThroughput member(s) ${coerced.dropped.join(", ")} in ${JSON.stringify(maskLeafValue$1(value, maskSecrets))} are not a number DynamoDB accepts, so they were dropped from the request, which leaves ${JSON.stringify(maskLeafValue$1(coerced.spec, maskSecrets))}. Check for an unresolved intrinsic or a non-numeric value.`);
17249
17360
  return coerced.spec;
17250
17361
  }
17251
17362
  /**
@@ -17895,18 +18006,17 @@ const DRIFT_STRIPPED_INDEX_MEMBERS = [{
17895
18006
  * by value rather than by position, so the two layers compose.
17896
18007
  *
17897
18008
  * Walks rather than testing the top level, because a secret nested in an object
17898
- * or array leaf is stringified — and escaped — identically. The sibling
17899
- * `dynamodb-table-provider.ts` carries the same helper; it is duplicated rather
17900
- * than shared because these two files deliberately hold no common module today
17901
- * beyond the narrow rule modules (`dynamodb-warm-throughput.ts`,
17902
- * `dynamodb-index-busy-delete.ts`), each of which exists for a MEASURED
17903
- * divergence rather than for convenience.
18009
+ * or array leaf is stringified — and escaped — identically.
18010
+ *
18011
+ * Issue #2176 hoisted the walk into `../masked-retry-logger.js`. The previous
18012
+ * note here said the helper was "duplicated rather than shared because these two
18013
+ * files deliberately hold no common module today" — that rationale expired the
18014
+ * moment a shared LEAF module existed for exactly this, and it had already cost
18015
+ * something: this copy carried NO depth cap, so it was one of SIX copies that
18016
+ * had drifted apart on the one property that matters.
17904
18017
  */
17905
18018
  function maskLeafValue(value, maskSecrets) {
17906
- if (typeof value === "string") return maskSecrets(value);
17907
- if (Array.isArray(value)) return value.map((v) => maskLeafValue(v, maskSecrets));
17908
- if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [maskSecrets(k), maskLeafValue(v, maskSecrets)]));
17909
- return value;
18019
+ return maskDeep(value, maskSecrets);
17910
18020
  }
17911
18021
  var DynamoDBGlobalTableProvider = class {
17912
18022
  dynamoDBClient;
@@ -22786,11 +22896,11 @@ var SSMParameterProvider = class {
22786
22896
  * whole body is wrapped: an unexpected failure degrades to "no Arn recorded"
22787
22897
  * exactly as the fabricated-account arm does.
22788
22898
  */
22789
- async buildParameterArn(name) {
22899
+ async buildParameterArn(name, mask) {
22790
22900
  try {
22791
- return await this.buildParameterArnUnguarded(name);
22901
+ return await this.buildParameterArnUnguarded(name, mask);
22792
22902
  } catch (error) {
22793
- this.logger.warn(`Could not build the Arn attribute for SSM parameter ${name}: ${error instanceof Error ? error.message : String(error)}. The parameter itself is unaffected; the Arn is NOT recorded, so an Fn::GetAtt on it will fail until a later deploy records it.`);
22903
+ this.logger.warn(mask(`Could not build the Arn attribute for SSM parameter ${name}: ${error instanceof Error ? error.message : String(error)}. The parameter itself is unaffected; the Arn is NOT recorded, so an Fn::GetAtt on it will fail until a later deploy records it.`));
22794
22904
  return;
22795
22905
  }
22796
22906
  }
@@ -22810,10 +22920,10 @@ var SSMParameterProvider = class {
22810
22920
  * import RECORDED it, so the fix belongs at the import boundary — where
22811
22921
  * `import()` now refuses it — and not here. Do not re-add it.
22812
22922
  */
22813
- async buildParameterArnUnguarded(name) {
22923
+ async buildParameterArnUnguarded(name, mask) {
22814
22924
  const accountInfo = await getAccountInfo(await this.ssmClient.config.region());
22815
22925
  if (accountInfo.fabricated) {
22816
- this.logger.warn(`Cannot determine the AWS account (STS is unreachable, and the resolved account id is a placeholder), so the Arn attribute for SSM parameter ${name} would be fabricated and is NOT recorded. An Fn::GetAtt on it will fail until a later deploy resolves the account.`);
22926
+ this.logger.warn(mask(`Cannot determine the AWS account (STS is unreachable, and the resolved account id is a placeholder), so the Arn attribute for SSM parameter ${name} would be fabricated and is NOT recorded. An Fn::GetAtt on it will fail until a later deploy resolves the account.`));
22817
22927
  return;
22818
22928
  }
22819
22929
  const { partition } = derivePartitionAndUrlSuffix(accountInfo.region);
@@ -22823,8 +22933,11 @@ var SSMParameterProvider = class {
22823
22933
  /**
22824
22934
  * Create an SSM parameter
22825
22935
  */
22826
- async create(logicalId, resourceType, properties) {
22827
- this.logger.debug(`Creating SSM parameter ${logicalId}`);
22936
+ async create(logicalId, resourceType, properties, context) {
22937
+ const mask = maskerOrIdentity(context?.maskSecrets);
22938
+ const warn = (message) => this.logger.warn(mask(message));
22939
+ const debug = (message) => this.logger.debug(mask(message));
22940
+ debug(`Creating SSM parameter ${logicalId}`);
22828
22941
  const name = properties["Name"] || `/${generateResourceName(logicalId, {
22829
22942
  maxLength: 1023,
22830
22943
  allowedPattern: /[^a-zA-Z0-9-/_]/g
@@ -22855,14 +22968,14 @@ var SSMParameterProvider = class {
22855
22968
  } catch (innerError) {
22856
22969
  try {
22857
22970
  await this.ssmClient.send(new DeleteParameterCommand({ Name: name }));
22858
- this.logger.debug(`Cleaned up partially-created SSM parameter ${logicalId} (${name}) after wiring failure`);
22971
+ debug(`Cleaned up partially-created SSM parameter ${logicalId} (${name}) after wiring failure`);
22859
22972
  } catch (cleanupError) {
22860
- this.logger.warn(`Failed to clean up partially-created SSM parameter ${logicalId} (${name}): ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}. Manual deletion may be required before the next deploy: aws ssm delete-parameter --name '${name}'`);
22973
+ warn(`Failed to clean up partially-created SSM parameter ${logicalId} (${name}): ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}. Manual deletion may be required before the next deploy: aws ssm delete-parameter --name '${name}'`);
22861
22974
  }
22862
22975
  throw innerError;
22863
22976
  }
22864
- this.logger.debug(`Successfully created SSM parameter ${logicalId}: ${name}`);
22865
- const arn = await this.buildParameterArn(name);
22977
+ debug(`Successfully created SSM parameter ${logicalId}: ${name}`);
22978
+ const arn = await this.buildParameterArn(name, mask);
22866
22979
  return {
22867
22980
  physicalId: name,
22868
22981
  attributes: {
@@ -22873,14 +22986,16 @@ var SSMParameterProvider = class {
22873
22986
  };
22874
22987
  } catch (error) {
22875
22988
  const cause = error instanceof Error ? error : void 0;
22876
- throw new ProvisioningError(`Failed to create SSM parameter ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
22989
+ throw new ProvisioningError(`Failed to create SSM parameter ${logicalId}: ${mask(error instanceof Error ? error.message : String(error))}`, resourceType, logicalId, void 0, cause);
22877
22990
  }
22878
22991
  }
22879
22992
  /**
22880
22993
  * Update an SSM parameter
22881
22994
  */
22882
- async update(logicalId, physicalId, resourceType, properties, previousProperties) {
22883
- this.logger.debug(`Updating SSM parameter ${logicalId}: ${physicalId}`);
22995
+ async update(logicalId, physicalId, resourceType, properties, previousProperties, context) {
22996
+ const mask = maskerOrIdentity(context?.maskSecrets);
22997
+ const debug = (message) => this.logger.debug(mask(message));
22998
+ debug(`Updating SSM parameter ${logicalId}: ${physicalId}`);
22884
22999
  const type = properties["Type"] || "String";
22885
23000
  const value = properties["Value"];
22886
23001
  if (!value) throw new ProvisioningError(`Value is required for SSM parameter ${logicalId}`, resourceType, logicalId, physicalId);
@@ -22912,10 +23027,10 @@ var SSMParameterProvider = class {
22912
23027
  ResourceId: physicalId,
22913
23028
  Tags: newTags
22914
23029
  }));
22915
- this.logger.debug(`Updated tags for SSM parameter ${physicalId}`);
23030
+ debug(`Updated tags for SSM parameter ${physicalId}`);
22916
23031
  }
22917
- this.logger.debug(`Successfully updated SSM parameter ${logicalId}`);
22918
- const arn = await this.buildParameterArn(physicalId);
23032
+ debug(`Successfully updated SSM parameter ${logicalId}`);
23033
+ const arn = await this.buildParameterArn(physicalId, mask);
22919
23034
  return {
22920
23035
  physicalId,
22921
23036
  wasReplaced: false,
@@ -22927,7 +23042,7 @@ var SSMParameterProvider = class {
22927
23042
  };
22928
23043
  } catch (error) {
22929
23044
  const cause = error instanceof Error ? error : void 0;
22930
- throw new ProvisioningError(`Failed to update SSM parameter ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
23045
+ throw new ProvisioningError(`Failed to update SSM parameter ${logicalId}: ${mask(error instanceof Error ? error.message : String(error))}`, resourceType, logicalId, physicalId, cause);
22931
23046
  }
22932
23047
  }
22933
23048
  /**
@@ -23919,9 +24034,10 @@ const generations = /* @__PURE__ */ new Map();
23919
24034
  * long-lived process cannot grow the map without bound.
23920
24035
  */
23921
24036
  const MAX_TRACKED_GENERATIONS = 1e5;
23922
- const tokenKey = (scope, logicalId) => `${scope}\u0000${process.env["AWS_REGION"] ?? ""}\u0000${getCurrentStackName() ?? ""}\u0000${logicalId}`;
23923
- const derive = (key, generation, maxLength) => {
23924
- return `cdkd-${createHash("sha256").update(`${PROCESS_NONCE}\u0000${key}\u0000${generation}`).digest("hex")}`.slice(0, maxLength);
24037
+ const tokenKey = (scope, logicalId, maxLength, charset) => `${scope}\u0000${process.env["AWS_REGION"] ?? ""}\u0000${getCurrentStackName() ?? ""}\u0000${logicalId}\u0000${maxLength}\u0000${charset}`;
24038
+ const derive = (key, generation, maxLength, charset) => {
24039
+ const digest = createHash("sha256").update(`${PROCESS_NONCE}\u0000${key}\u0000${generation}`).digest("hex");
24040
+ return `cdkd${charset === "alphanumeric" ? "" : "-"}${digest}`.slice(0, maxLength);
23925
24041
  };
23926
24042
  /**
23927
24043
  * Get the idempotency token for one logical create, minting it on first use and
@@ -23935,10 +24051,10 @@ const derive = (key, generation, maxLength) => {
23935
24051
  * ```
23936
24052
  */
23937
24053
  function acquireIdempotencyToken(options) {
23938
- const { scope, logicalId, maxLength = 64 } = options;
23939
- const key = tokenKey(scope, logicalId);
24054
+ const { scope, logicalId, maxLength = 64, charset = "default" } = options;
24055
+ const key = tokenKey(scope, logicalId, maxLength, charset);
23940
24056
  const existing = inFlight.get(key);
23941
- const value = existing ?? derive(key, generations.get(key) ?? 0, maxLength);
24057
+ const value = existing ?? derive(key, generations.get(key) ?? 0, maxLength, charset);
23942
24058
  if (existing === void 0) inFlight.set(key, value);
23943
24059
  return {
23944
24060
  value,
@@ -24401,10 +24517,10 @@ var EC2Provider = class {
24401
24517
  case "AWS::EC2::VPCGatewayAttachment": return this.createVpcGatewayAttachment(logicalId, resourceType, properties, context);
24402
24518
  case "AWS::EC2::NatGateway": return this.createNatGateway(logicalId, resourceType, properties);
24403
24519
  case "AWS::EC2::RouteTable": return this.createRouteTable(logicalId, resourceType, properties);
24404
- case "AWS::EC2::Route": return this.createRoute(logicalId, resourceType, properties, context?.replayingState === true ? (message) => this.logger.warn(message) : void 0);
24520
+ case "AWS::EC2::Route": return this.createRoute(logicalId, resourceType, properties, context?.replayingState === true ? (message) => this.logger.warn(message) : void 0, context?.maskSecrets);
24405
24521
  case "AWS::EC2::SubnetRouteTableAssociation": return this.createSubnetRouteTableAssociation(logicalId, resourceType, properties);
24406
24522
  case "AWS::EC2::SecurityGroup": return this.createSecurityGroup(logicalId, resourceType, properties);
24407
- case "AWS::EC2::SecurityGroupIngress": return this.createSecurityGroupIngress(logicalId, resourceType, properties, context?.replayingState === true ? (message) => this.logger.warn(message) : void 0);
24523
+ case "AWS::EC2::SecurityGroupIngress": return this.createSecurityGroupIngress(logicalId, resourceType, properties, context?.replayingState === true ? (message) => this.logger.warn(message) : void 0, context?.maskSecrets);
24408
24524
  case "AWS::EC2::Instance": return this.createInstance(logicalId, resourceType, properties, context);
24409
24525
  case "AWS::EC2::NetworkAcl": return this.createNetworkAcl(logicalId, resourceType, properties);
24410
24526
  case "AWS::EC2::NetworkAclEntry": return this.createNetworkAclEntry(logicalId, resourceType, properties, context);
@@ -24412,16 +24528,16 @@ var EC2Provider = class {
24412
24528
  default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId);
24413
24529
  }
24414
24530
  }
24415
- async update(logicalId, physicalId, resourceType, properties, previousProperties) {
24531
+ async update(logicalId, physicalId, resourceType, properties, previousProperties, context) {
24416
24532
  try {
24417
- return await this.applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties);
24533
+ return await this.applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties, context);
24418
24534
  } catch (error) {
24419
24535
  if (error instanceof CdkdError) throw error;
24420
24536
  const cause = error instanceof Error ? error : void 0;
24421
24537
  throw new ProvisioningError(`Failed to update EC2 resource ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
24422
24538
  }
24423
24539
  }
24424
- async applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties) {
24540
+ async applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties, context) {
24425
24541
  switch (resourceType) {
24426
24542
  case "AWS::EC2::VPC": return this.updateVpc(logicalId, physicalId, resourceType, properties, previousProperties);
24427
24543
  case "AWS::EC2::Subnet": return this.updateSubnet(logicalId, physicalId, resourceType, properties, previousProperties);
@@ -24430,10 +24546,10 @@ var EC2Provider = class {
24430
24546
  case "AWS::EC2::VPCGatewayAttachment": return this.updateVpcGatewayAttachment(logicalId, physicalId);
24431
24547
  case "AWS::EC2::NatGateway": return this.updateNatGateway(logicalId, physicalId);
24432
24548
  case "AWS::EC2::RouteTable": return this.updateRouteTable(logicalId, physicalId);
24433
- case "AWS::EC2::Route": return this.updateRoute(logicalId, physicalId, resourceType, properties, previousProperties);
24549
+ case "AWS::EC2::Route": return this.updateRoute(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
24434
24550
  case "AWS::EC2::SubnetRouteTableAssociation": return this.updateSubnetRouteTableAssociation(logicalId, physicalId);
24435
24551
  case "AWS::EC2::SecurityGroup": return this.updateSecurityGroup(logicalId, physicalId, resourceType, properties, previousProperties);
24436
- case "AWS::EC2::SecurityGroupIngress": return this.updateSecurityGroupIngress(logicalId, physicalId, resourceType, properties, previousProperties);
24552
+ case "AWS::EC2::SecurityGroupIngress": return this.updateSecurityGroupIngress(logicalId, physicalId, resourceType, properties, previousProperties, context?.maskSecrets);
24437
24553
  case "AWS::EC2::Instance": return this.updateInstance(logicalId, physicalId, resourceType, properties, previousProperties);
24438
24554
  case "AWS::EC2::NetworkAcl":
24439
24555
  case "AWS::EC2::NetworkAclEntry":
@@ -24897,7 +25013,10 @@ var EC2Provider = class {
24897
25013
  throw new ProvisioningError(`Failed to create EIP ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
24898
25014
  }
24899
25015
  return {
24900
- physicalId: this.eipPhysicalId(logicalId, publicIp, allocationId, context?.replayingState === true ? { onRefusal: (message) => this.logger.warn(message) } : void 0),
25016
+ physicalId: this.eipPhysicalId(logicalId, publicIp, allocationId, {
25017
+ maskSecrets: context?.maskSecrets,
25018
+ ...context?.replayingState === true && { onRefusal: (message) => this.logger.warn(message) }
25019
+ }),
24901
25020
  attributes: {
24902
25021
  AllocationId: allocationId,
24903
25022
  PublicIp: publicIp
@@ -24979,7 +25098,10 @@ var EC2Provider = class {
24979
25098
  }, {
24980
25099
  name: "vpcId",
24981
25100
  value: vpcId
24982
- }], context?.replayingState === true ? { onRefusal: (message) => this.logger.warn(message) } : void 0);
25101
+ }], {
25102
+ maskSecrets: context?.maskSecrets,
25103
+ ...context?.replayingState === true && { onRefusal: (message) => this.logger.warn(message) }
25104
+ });
24983
25105
  try {
24984
25106
  await this.ec2Client.send(new AttachInternetGatewayCommand({
24985
25107
  VpcId: vpcId,
@@ -25132,7 +25254,7 @@ var EC2Provider = class {
25132
25254
  throw new ProvisioningError(`Failed to delete RouteTable ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
25133
25255
  }
25134
25256
  }
25135
- async createRoute(logicalId, resourceType, properties, onMultipleDestinations) {
25257
+ async createRoute(logicalId, resourceType, properties, onMultipleDestinations, maskSecrets) {
25136
25258
  this.logger.debug(`Creating Route ${logicalId}`);
25137
25259
  const routeTableId = properties["RouteTableId"];
25138
25260
  const destinationCidrBlock = properties["DestinationCidrBlock"];
@@ -25156,7 +25278,10 @@ var EC2Provider = class {
25156
25278
  }, {
25157
25279
  name: "destination",
25158
25280
  value: destination
25159
- }], onMultipleDestinations ? { onRefusal: onMultipleDestinations } : void 0);
25281
+ }], {
25282
+ maskSecrets,
25283
+ ...onMultipleDestinations && { onRefusal: onMultipleDestinations }
25284
+ });
25160
25285
  try {
25161
25286
  await this.ec2Client.send(new CreateRouteCommand({
25162
25287
  RouteTableId: routeTableId,
@@ -25184,7 +25309,7 @@ var EC2Provider = class {
25184
25309
  throw new ProvisioningError(`Failed to create Route ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
25185
25310
  }
25186
25311
  }
25187
- async updateRoute(logicalId, physicalId, resourceType, properties, previousProperties) {
25312
+ async updateRoute(logicalId, physicalId, resourceType, properties, previousProperties, maskSecrets) {
25188
25313
  this.logger.debug(`Updating Route ${logicalId}: ${physicalId}`);
25189
25314
  if (JSON.stringify(properties) === JSON.stringify(previousProperties)) return {
25190
25315
  physicalId,
@@ -25192,7 +25317,7 @@ var EC2Provider = class {
25192
25317
  };
25193
25318
  try {
25194
25319
  await this.deleteRoute(logicalId, physicalId, resourceType);
25195
- const createResult = await this.createRoute(logicalId, resourceType, properties, (message) => this.logger.warn(message));
25320
+ const createResult = await this.createRoute(logicalId, resourceType, properties, (message) => this.logger.warn(message), maskSecrets);
25196
25321
  return {
25197
25322
  physicalId: createResult.physicalId,
25198
25323
  wasReplaced: true,
@@ -25475,7 +25600,7 @@ var EC2Provider = class {
25475
25600
  * and on a rollback replay the value comes from a STATE record, which no
25476
25601
  * template edit can fix. Absent (the plain CREATE dispatch) it throws.
25477
25602
  */
25478
- async createSecurityGroupIngress(logicalId, resourceType, properties, onUnusableProtocol) {
25603
+ async createSecurityGroupIngress(logicalId, resourceType, properties, onUnusableProtocol, maskSecrets) {
25479
25604
  this.logger.debug(`Creating SecurityGroupIngress ${logicalId}`);
25480
25605
  const groupId = properties["GroupId"];
25481
25606
  if (!groupId) throw new ProvisioningError(`GroupId is required for SecurityGroupIngress ${logicalId}`, resourceType, logicalId);
@@ -25500,7 +25625,10 @@ var EC2Provider = class {
25500
25625
  name: "toPort",
25501
25626
  value: toPort ?? "-1"
25502
25627
  }
25503
- ], onUnusableProtocol ? { onRefusal: onUnusableProtocol } : void 0);
25628
+ ], {
25629
+ maskSecrets,
25630
+ ...onUnusableProtocol && { onRefusal: onUnusableProtocol }
25631
+ });
25504
25632
  try {
25505
25633
  const ruleId = singleSecurityGroupRuleId((await this.ec2Client.send(new AuthorizeSecurityGroupIngressCommand({
25506
25634
  GroupId: groupId,
@@ -25596,7 +25724,7 @@ var EC2Provider = class {
25596
25724
  return;
25597
25725
  }
25598
25726
  }
25599
- async updateSecurityGroupIngress(logicalId, physicalId, resourceType, properties, previousProperties) {
25727
+ async updateSecurityGroupIngress(logicalId, physicalId, resourceType, properties, previousProperties, maskSecrets) {
25600
25728
  this.logger.debug(`Updating SecurityGroupIngress ${logicalId}: ${physicalId}`);
25601
25729
  if (JSON.stringify(properties) === JSON.stringify(previousProperties)) return {
25602
25730
  physicalId,
@@ -25604,7 +25732,7 @@ var EC2Provider = class {
25604
25732
  };
25605
25733
  try {
25606
25734
  await this.deleteSecurityGroupIngress(logicalId, physicalId, resourceType, previousProperties);
25607
- const createResult = await this.createSecurityGroupIngress(logicalId, resourceType, properties, (message) => this.logger.warn(message));
25735
+ const createResult = await this.createSecurityGroupIngress(logicalId, resourceType, properties, (message) => this.logger.warn(message), maskSecrets);
25608
25736
  return {
25609
25737
  physicalId: createResult.physicalId,
25610
25738
  wasReplaced: true,
@@ -26288,7 +26416,10 @@ var EC2Provider = class {
26288
26416
  name: "egress",
26289
26417
  value: egress
26290
26418
  }
26291
- ], context?.replayingState === true ? { onRefusal: (message) => this.logger.warn(message) } : void 0);
26419
+ ], {
26420
+ maskSecrets: context?.maskSecrets,
26421
+ ...context?.replayingState === true && { onRefusal: (message) => this.logger.warn(message) }
26422
+ });
26292
26423
  try {
26293
26424
  const cidrBlock = properties["CidrBlock"];
26294
26425
  const ipv6CidrBlock = properties["Ipv6CidrBlock"];
@@ -28337,7 +28468,10 @@ var ApiGatewayProvider = class ApiGatewayProvider {
28337
28468
  name: "httpMethod",
28338
28469
  value: httpMethod
28339
28470
  }
28340
- ], context?.replayingState === true ? { onRefusal: (message) => this.logger.warn(message) } : void 0);
28471
+ ], {
28472
+ maskSecrets: context?.maskSecrets,
28473
+ ...context?.replayingState === true && { onRefusal: (message) => this.logger.warn(message) }
28474
+ });
28341
28475
  try {
28342
28476
  await this.apiGatewayClient.send(new PutMethodCommand({
28343
28477
  restApiId,
@@ -30381,12 +30515,7 @@ var ApiGatewayV2Provider = class {
30381
30515
  */
30382
30516
  toSdkResponseParameters(value, maskSecrets = (text) => text) {
30383
30517
  const warn = (message) => this.logger.warn(maskSecrets(message));
30384
- const maskLeaf = (value) => {
30385
- if (typeof value === "string") return maskSecrets(value);
30386
- if (Array.isArray(value)) return value.map(maskLeaf);
30387
- if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [maskSecrets(k), maskLeaf(v)]));
30388
- return value;
30389
- };
30518
+ const maskLeaf = (value) => maskDeep(value, maskSecrets);
30390
30519
  if (value == null) return void 0;
30391
30520
  if (typeof value !== "object" || Array.isArray(value)) return value;
30392
30521
  const out = {};
@@ -34955,44 +35084,6 @@ var ECSProvider = class {
34955
35084
  }
34956
35085
  };
34957
35086
 
34958
- //#endregion
34959
- //#region src/provisioning/masked-retry-logger.ts
34960
- /**
34961
- * The masker a caller supplied, or the identity function when it supplied
34962
- * none.
34963
- *
34964
- * ABSENT MEANS UNMASKED, and that is the back-compatible default the contract
34965
- * mandates — `create()` / `update()` are also reached from the import path,
34966
- * from `cdkd drift --revert`, and from tests, and a provider must not care
34967
- * which caller it got. Centralised here so no call site re-spells the `??`
34968
- * and accidentally makes the capability required.
34969
- */
34970
- function maskerOrIdentity(maskSecrets) {
34971
- return maskSecrets ?? ((text) => text);
34972
- }
34973
- /**
34974
- * A {@link RetryLogger} whose every line is routed through `maskSecrets`.
34975
- *
34976
- * `warn` is ALWAYS provided, never omitted. It is optional on `RetryLogger`,
34977
- * and dropping it would silence the give-up summary on the calling path only —
34978
- * trading a disclosure for the reporting hole issue #2018 closed. It goes
34979
- * through the SAME mask as `debug` precisely because it is the line that
34980
- * survives a run without `--verbose`; forwarding it unmasked would defeat the
34981
- * fence at a HIGHER log level than the one the fence was written for.
34982
- *
34983
- * `logger` is typed structurally rather than as the concrete `Logger` so this
34984
- * module stays a leaf and a test can pass two spies. That matches the
34985
- * precedent already in the tree (`buildMfaConfigRequest` in
34986
- * `cognito-provider.ts` takes an injected `logger?: { warn }`).
34987
- */
34988
- function createMaskedRetryLogger(logger, maskSecrets) {
34989
- const mask = maskerOrIdentity(maskSecrets);
34990
- return {
34991
- debug: (message) => logger.debug(mask(message)),
34992
- warn: (message) => logger.warn(mask(message))
34993
- };
34994
- }
34995
-
34996
35087
  //#endregion
34997
35088
  //#region src/provisioning/providers/elbv2-provider.ts
34998
35089
  /**
@@ -35002,41 +35093,6 @@ function createMaskedRetryLogger(logger, maskSecrets) {
35002
35093
  */
35003
35094
  const capacityReservationDelays = { sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) };
35004
35095
  /**
35005
- * Depth cap for {@link maskDeep}. Bounds a self-referential bag rather than
35006
- * hanging; `JSON.stringify` would throw on one anyway, and a throw is the
35007
- * better failure of the two.
35008
- */
35009
- const MASK_WALK_MAX_DEPTH$1 = 8;
35010
- /**
35011
- * Mask every string LEAF and KEY of an arbitrary value, returning a structure
35012
- * safe to `JSON.stringify` into a log line (issue #2050).
35013
- *
35014
- * The ordering is the whole point. `maskSecretsInText` matches by literal
35015
- * occurrence, and `JSON.stringify` escapes `"`, `\` and newlines — so a Secrets
35016
- * Manager JSON document (the commonest real secret shape) does not occur in the
35017
- * stringified text and survives a mask applied afterwards. Masking the leaves
35018
- * first also hands the masker each RAW value, which reaches
35019
- * `maskSecretsInText`'s whole-value arm at any length instead of only the
35020
- * substring arm's 4-character floor. See the "three gaps" note in
35021
- * `src/deployment/secret-redaction.ts`.
35022
- *
35023
- * Keys are masked as well as values: `JSON.stringify` renders them into the
35024
- * same line, so a resolved secret used as a map key would otherwise escape.
35025
- *
35026
- * `cognito-provider.ts` carries an equivalent private walk for its own
35027
- * stringified warnings. Two copies is the current cost of keeping this a leaf;
35028
- * a THIRD site is the point at which this should move into
35029
- * `../masked-retry-logger.ts` (already the shared provider-side masking module)
35030
- * and all three converge on it.
35031
- */
35032
- function maskDeep(value, mask, depth = 0) {
35033
- if (typeof value === "string") return mask(value);
35034
- if (depth >= MASK_WALK_MAX_DEPTH$1) return value;
35035
- if (Array.isArray(value)) return value.map((entry) => maskDeep(entry, mask, depth + 1));
35036
- if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [mask(k), maskDeep(v, mask, depth + 1)]));
35037
- return value;
35038
- }
35039
- /**
35040
35096
  * Documented AWS defaults for TargetGroup attributes, used to reset a
35041
35097
  * removed `TargetGroupAttributes` entry. Unlike ModifyLoadBalancerAttributes
35042
35098
  * / ModifyListenerAttributes, ModifyTargetGroupAttributes REJECTS an empty
@@ -39580,10 +39636,10 @@ var Route53Provider = class {
39580
39636
  default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId);
39581
39637
  }
39582
39638
  }
39583
- async update(logicalId, physicalId, resourceType, properties, previousProperties) {
39639
+ async update(logicalId, physicalId, resourceType, properties, previousProperties, context) {
39584
39640
  switch (resourceType) {
39585
39641
  case "AWS::Route53::HostedZone": return this.updateHostedZone(logicalId, physicalId, resourceType, properties, previousProperties);
39586
- case "AWS::Route53::RecordSet": return this.updateRecordSet(logicalId, physicalId, resourceType, properties);
39642
+ case "AWS::Route53::RecordSet": return this.updateRecordSet(logicalId, physicalId, resourceType, properties, context);
39587
39643
  default: throw new ProvisioningError(`Unsupported resource type: ${resourceType}`, resourceType, logicalId, physicalId);
39588
39644
  }
39589
39645
  }
@@ -39901,7 +39957,10 @@ var Route53Provider = class {
39901
39957
  name: "recordType",
39902
39958
  value: recordType
39903
39959
  }
39904
- ], context?.replayingState === true ? { onRefusal: (message) => this.logger.warn(message) } : void 0);
39960
+ ], {
39961
+ maskSecrets: context?.maskSecrets,
39962
+ ...context?.replayingState === true && { onRefusal: (message) => this.logger.warn(message) }
39963
+ });
39905
39964
  try {
39906
39965
  const resourceRecordSet = this.buildResourceRecordSet(properties);
39907
39966
  const comment = properties["Comment"];
@@ -39926,7 +39985,7 @@ var Route53Provider = class {
39926
39985
  throw new ProvisioningError(`Failed to create record set ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
39927
39986
  }
39928
39987
  }
39929
- async updateRecordSet(logicalId, physicalId, resourceType, properties) {
39988
+ async updateRecordSet(logicalId, physicalId, resourceType, properties, context) {
39930
39989
  this.logger.debug(`Updating Route 53 record set ${logicalId}: ${physicalId}`);
39931
39990
  const hostedZoneId = await this.resolveHostedZoneId(properties, logicalId, resourceType, physicalId);
39932
39991
  const recordName = properties["Name"];
@@ -39944,7 +40003,10 @@ var Route53Provider = class {
39944
40003
  name: "recordType",
39945
40004
  value: recordType
39946
40005
  }
39947
- ], { onRefusal: (message) => this.logger.warn(message) });
40006
+ ], {
40007
+ onRefusal: (message) => this.logger.warn(message),
40008
+ maskSecrets: context?.maskSecrets
40009
+ });
39948
40010
  try {
39949
40011
  const resourceRecordSet = this.buildResourceRecordSet(properties);
39950
40012
  const comment = properties["Comment"];
@@ -40977,15 +41039,6 @@ function readDeclaredMfaConfiguration(raw, options) {
40977
41039
  return value.trim() === "" ? "" : value;
40978
41040
  }
40979
41041
  /**
40980
- * Depth cap for the pre-stringify secret walk in {@link buildMfaConfigRequest}.
40981
- *
40982
- * Only a guard against a self-referential bag, not a real bound: a resolved
40983
- * `EnabledMfas` is a list of factor-name scalars, so the walk never reaches
40984
- * depth 2 on any shape this warning exists to describe. Deliberately generous
40985
- * so no plausible mis-shape is left unmasked by it.
40986
- */
40987
- const MASK_WALK_MAX_DEPTH = 8;
40988
- /**
40989
41042
  * Read `EnabledMfas` as the pair (recognized list, "the template asked for a
40990
41043
  * factor at all").
40991
41044
  *
@@ -41119,15 +41172,9 @@ function buildMfaConfigRequest(physicalId, properties, logger, declaredMfaConfig
41119
41172
  request.MfaConfiguration = declaredMfaConfiguration || (enablesMfaFactor ? "OPTIONAL" : "OFF");
41120
41173
  if (enabledMfasBlank) warn(`UserPool ${physicalId}: EnabledMfas is an empty string, which declares no MFA factor and is treated as absent — a mangled Fn::Join or an empty String parameter collapses to this shape. Nothing is enabled from it and MfaConfiguration is ${request.MfaConfiguration}; declare the factor names as a list (e.g. ["${MFA_FACTOR_SOFTWARE_TOKEN}"]) if a factor was intended.`);
41121
41174
  const anyFactorBlock = hasAnyMfaFactorBlock(request);
41122
- const maskDeep = (v, depth = 0) => {
41123
- if (typeof v === "string") return maskSecrets(v);
41124
- if (depth >= MASK_WALK_MAX_DEPTH) return v;
41125
- if (Array.isArray(v)) return v.map((e) => maskDeep(e, depth + 1));
41126
- if (v !== null && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [maskSecrets(k), maskDeep(x, depth + 1)]));
41127
- return v;
41128
- };
41129
- const dropped = unrecognizedFactors.map((f) => JSON.stringify(maskDeep(f)) ?? String(f));
41130
- if (enabledMfasMalformed) dropped.push(`${JSON.stringify(maskDeep(properties["EnabledMfas"]))} (not a list)`);
41175
+ const maskLeaves = (v) => maskDeep(v, maskSecrets);
41176
+ const dropped = unrecognizedFactors.map((f) => JSON.stringify(maskLeaves(f)) ?? String(f));
41177
+ if (enabledMfasMalformed) dropped.push(`${JSON.stringify(maskLeaves(properties["EnabledMfas"]))} (not a list)`);
41131
41178
  if (dropped.length > 0) {
41132
41179
  const consequence = request.MfaConfiguration === "OFF" ? `MfaConfiguration is OFF, so this pool deploys with MFA DISABLED` : anyFactorBlock ? `MfaConfiguration is ${request.MfaConfiguration} and another factor block IS sent, so these entries are silently ignored rather than failing the call on their own` : `MfaConfiguration is ${request.MfaConfiguration} with no factor enabled, so AWS rejects this call rather than deploying the pool with MFA disabled`;
41133
41180
  warn(`UserPool ${physicalId}: EnabledMfas entries ${dropped.join(", ")} do not map to an MFA factor block (known: ${MFA_FACTOR_SMS}, ${MFA_FACTOR_SOFTWARE_TOKEN}, ${MFA_FACTOR_EMAIL_OTP}); no factor is enabled from them. ${consequence}.`);
@@ -44885,7 +44932,10 @@ var AppSyncProvider = class {
44885
44932
  }, {
44886
44933
  name: "name",
44887
44934
  value: name
44888
- }], context?.replayingState === true ? { onRefusal: (message) => this.logger.warn(message) } : void 0);
44935
+ }], {
44936
+ maskSecrets: context?.maskSecrets,
44937
+ ...context?.replayingState === true && { onRefusal: (message) => this.logger.warn(message) }
44938
+ });
44889
44939
  let reportedArn;
44890
44940
  try {
44891
44941
  const input = {
@@ -44951,7 +45001,10 @@ var AppSyncProvider = class {
44951
45001
  name: "fieldName",
44952
45002
  value: fieldName
44953
45003
  }
44954
- ], context?.replayingState === true ? { onRefusal: (message) => this.logger.warn(message) } : void 0);
45004
+ ], {
45005
+ maskSecrets: context?.maskSecrets,
45006
+ ...context?.replayingState === true && { onRefusal: (message) => this.logger.warn(message) }
45007
+ });
44955
45008
  let reportedArn;
44956
45009
  try {
44957
45010
  const input = {
@@ -45018,7 +45071,10 @@ var AppSyncProvider = class {
45018
45071
  }, {
45019
45072
  name: "apiKeyId",
45020
45073
  value: apiKeyId
45021
- }], context?.replayingState === true ? { onRefusal: (message) => this.logger.warn(message) } : void 0);
45074
+ }], {
45075
+ maskSecrets: context?.maskSecrets,
45076
+ ...context?.replayingState === true && { onRefusal: (message) => this.logger.warn(message) }
45077
+ });
45022
45078
  const apiKeyArn = await this.buildAppSyncArnOrWarn(`apis/${apiId}/apikey/${apiKeyId}`, logicalId, physicalId);
45023
45079
  return {
45024
45080
  physicalId,
@@ -45704,7 +45760,10 @@ var GlueProvider = class {
45704
45760
  }, {
45705
45761
  name: "tableName",
45706
45762
  value: tableName
45707
- }], context?.replayingState === true ? { onRefusal: (message) => this.logger.warn(message) } : void 0);
45763
+ }], {
45764
+ maskSecrets: context?.maskSecrets,
45765
+ ...context?.replayingState === true && { onRefusal: (message) => this.logger.warn(message) }
45766
+ });
45708
45767
  try {
45709
45768
  await this.getClient().send(new CreateTableCommand$1({
45710
45769
  CatalogId: catalogId,
@@ -57184,7 +57243,10 @@ var S3TablesProvider = class {
57184
57243
  }, {
57185
57244
  name: "namespace",
57186
57245
  value: namespaceName
57187
- }], context?.replayingState === true ? { onRefusal: (message) => this.logger.warn(message) } : void 0);
57246
+ }], {
57247
+ maskSecrets: context?.maskSecrets,
57248
+ ...context?.replayingState === true && { onRefusal: (message) => this.logger.warn(message) }
57249
+ });
57188
57250
  try {
57189
57251
  await this.getClient().send(new CreateNamespaceCommand({
57190
57252
  tableBucketARN,
@@ -57244,7 +57306,10 @@ var S3TablesProvider = class {
57244
57306
  name: "name",
57245
57307
  value: name
57246
57308
  }
57247
- ], context?.replayingState === true ? { onRefusal: (message) => this.logger.warn(message) } : void 0);
57309
+ ], {
57310
+ maskSecrets: context?.maskSecrets,
57311
+ ...context?.replayingState === true && { onRefusal: (message) => this.logger.warn(message) }
57312
+ });
57248
57313
  try {
57249
57314
  const response = await this.getClient().send(new CreateTableCommand$2({
57250
57315
  tableBucketARN,
@@ -59380,6 +59445,19 @@ function isCertificateInUseError(error) {
59380
59445
  * consumers (CloudFront, ALB) will fail to start if they reach the cert
59381
59446
  * before it issues, but that's the documented trade-off.
59382
59447
  *
59448
+ * **A create that fails after requesting DELETES its certificate** (issue
59449
+ * [#2169](https://github.com/go-to-k/cdkd/issues/2169)). `RequestCertificate`
59450
+ * materializes the certificate immediately and everything after it can fail —
59451
+ * most commonly the ISSUED wait, which a DNS-validated certificate whose
59452
+ * validation records are not live yet exhausts by construction. Before this,
59453
+ * the ARN was thrown away with the error: the certificate existed in AWS with
59454
+ * nothing naming it (absent from state, so invisible to `cdkd state show` and
59455
+ * unreachable by `cdkd destroy`), and the next `cdkd deploy` requested another
59456
+ * one — an orphan per attempt. `create()` now retires the certificate it just
59457
+ * requested before re-throwing, so a failed deploy leaves nothing behind and a
59458
+ * retry starts clean. See {@link ACMCertificateProvider.cleanupRequestedCertificate}
59459
+ * for why deleting is safe and why it is preferred over recording the remnant.
59460
+ *
59383
59461
  * **CloudFront cross-region note**: ACM certificates referenced by a
59384
59462
  * CloudFront Distribution MUST live in `us-east-1`. cdkd does not enforce
59385
59463
  * this — it's the developer's responsibility to deploy the certificate
@@ -59429,12 +59507,22 @@ var ACMCertificateProvider = class {
59429
59507
  if (Object.keys(options).length > 0) input["Options"] = options;
59430
59508
  const tags = properties["Tags"];
59431
59509
  if (tags && Array.isArray(tags) && tags.length > 0) input["Tags"] = tags;
59510
+ const idempotencyToken = acquireIdempotencyToken({
59511
+ scope: "RequestCertificate",
59512
+ logicalId,
59513
+ maxLength: 32,
59514
+ charset: "alphanumeric"
59515
+ });
59516
+ input["IdempotencyToken"] = idempotencyToken.value;
59517
+ let requestedArn;
59432
59518
  try {
59433
59519
  const certificateArn = (await this.acmClient.send(new RequestCertificateCommand(input))).CertificateArn;
59434
59520
  if (!certificateArn) throw new ProvisioningError(`RequestCertificate succeeded but no CertificateArn returned for ${logicalId}`, resourceType, logicalId);
59521
+ requestedArn = certificateArn;
59435
59522
  this.logger.debug(`Requested ACM certificate: ${certificateArn}`);
59436
- if (!(process.env["CDKD_NO_WAIT"] === "true")) await this.waitForCertificateIssued(certificateArn, logicalId);
59523
+ if (!(process.env["CDKD_NO_WAIT"] === "true")) await this.waitForCertificateIssued(certificateArn, logicalId, resourceType);
59437
59524
  else this.logger.warn(`Skipping wait for ACM certificate ${logicalId} (CDKD_NO_WAIT=true). Downstream consumers (CloudFront / ALB) will fail until the cert reaches ISSUED.`);
59525
+ idempotencyToken.release();
59438
59526
  return {
59439
59527
  physicalId: certificateArn,
59440
59528
  attributes: {
@@ -59443,9 +59531,84 @@ var ACMCertificateProvider = class {
59443
59531
  }
59444
59532
  };
59445
59533
  } catch (error) {
59446
- if (error instanceof CdkdError) throw error;
59534
+ const survivorNote = requestedArn ? await this.cleanupRequestedCertificate(requestedArn, logicalId, idempotencyToken) : void 0;
59535
+ if (error instanceof CdkdError) {
59536
+ if (survivorNote === void 0) throw error;
59537
+ throw new ProvisioningError(`${error.message} ${survivorNote}`, resourceType, logicalId, requestedArn, error);
59538
+ }
59447
59539
  const cause = error instanceof Error ? error : void 0;
59448
- throw new ProvisioningError(`Failed to create ACM certificate ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
59540
+ throw new ProvisioningError(`Failed to create ACM certificate ${logicalId}: ${error instanceof Error ? error.message : String(error)}` + (survivorNote === void 0 ? "" : ` ${survivorNote}`), resourceType, logicalId, requestedArn, cause);
59541
+ }
59542
+ }
59543
+ /**
59544
+ * Retire the certificate a FAILED `create()` just requested, so the failure
59545
+ * leaves nothing behind (issue
59546
+ * [#2169](https://github.com/go-to-k/cdkd/issues/2169)).
59547
+ *
59548
+ * Returns `undefined` when the certificate is gone, or a one-line note naming
59549
+ * the survivor when it could not be deleted. Never throws: the original
59550
+ * create error is what the user needs to see, and a cleanup failure must
59551
+ * degrade that message rather than replace it.
59552
+ *
59553
+ * ## Why DELETING is right, when the obvious objection is that it throws away
59554
+ * the user's DNS work
59555
+ *
59556
+ * It does not. ACM derives a domain's validation CNAME from the domain and
59557
+ * the account, not from the certificate, and documents that the record "must
59558
+ * be added to your DNS database only once": *"Without the need to repeat
59559
+ * validation, you can request additional ACM certificates for your fully
59560
+ * qualified domain name (FQDN) for as long as the CNAME record remains in
59561
+ * place ... You can also replace a deleted certificate."*
59562
+ * (https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html)
59563
+ * So the CNAMEs a user adds after seeing this failure validate the NEXT
59564
+ * certificate too, and the retry issues normally. The intuition that a
59565
+ * `PENDING_VALIDATION` certificate is precious does not hold for ACM.
59566
+ *
59567
+ * ## Why not RECORD the remnant in cdkd state instead
59568
+ *
59569
+ * That was this fix's first shape, and it does not work. A state record makes
59570
+ * the next deploy diff the resource as `NO_CHANGE` -- the recorded properties
59571
+ * ARE the template's -- so cdkd would never touch that certificate again:
59572
+ * `cdkd deploy` would print "No changes detected" and exit 0 over a
59573
+ * certificate that is still unusable, and the CloudFront / ALB consuming it
59574
+ * would fail with no explanation. Turning a loud failure into a silent one is
59575
+ * worse than the orphan it fixes. Making the diff re-provision it instead
59576
+ * needs a marker on the state record -- a schema bump -- to buy behaviour
59577
+ * that deleting gives for free.
59578
+ *
59579
+ * ## The "keep it" escape hatch already exists
59580
+ *
59581
+ * A user who WANTS the certificate to outlive the deploy has
59582
+ * `CDKD_NO_WAIT=true` (`cdkd deploy --no-wait`), which returns success with
59583
+ * the certificate recorded in state and never reaches this path. "Wait for
59584
+ * it" and "keep it even if the wait fails" are different requests, and cdkd
59585
+ * already has a flag for the second.
59586
+ */
59587
+ /**
59588
+ * The region an ACM ARN names, for the manual retire command. Taken from the
59589
+ * ARN rather than from the client so the command a user pastes matches the
59590
+ * certificate it names even when their shell default differs. Falls back to
59591
+ * the literal placeholder for a shape this cannot parse -- a wrong region
59592
+ * would make the command silently address a different account/region.
59593
+ */
59594
+ regionOfArn(arn) {
59595
+ return arn.split(":")[3] || "<region>";
59596
+ }
59597
+ async cleanupRequestedCertificate(certificateArn, logicalId, idempotencyToken) {
59598
+ try {
59599
+ await this.acmClient.send(new DeleteCertificateCommand({ CertificateArn: certificateArn }));
59600
+ this.logger.info(`Deleted the ACM certificate ${certificateArn} that the failed create of ${logicalId} requested, so nothing is left behind. DNS validation records you have already added stay valid for the retry.`);
59601
+ idempotencyToken.release();
59602
+ return;
59603
+ } catch (cleanupError) {
59604
+ if (cleanupError instanceof ResourceNotFoundException$7 || cleanupError instanceof Error && cleanupError.name === "ResourceNotFoundException") {
59605
+ this.logger.info(`The ACM certificate ${certificateArn} that the failed create of ${logicalId} requested was already gone; nothing to clean up.`);
59606
+ idempotencyToken.release();
59607
+ return;
59608
+ }
59609
+ const detail = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
59610
+ this.logger.warn(`Could not delete the ACM certificate ${certificateArn} that the failed create of ${logicalId} requested: ${detail}`);
59611
+ return `The certificate ${certificateArn} this attempt created could NOT be deleted, and cdkd is not tracking it -- retire it with \`aws acm delete-certificate --certificate-arn ${certificateArn} --region ${this.regionOfArn(certificateArn)}\` (the reason is in the warning above).`;
59449
59612
  }
59450
59613
  }
59451
59614
  async update(logicalId, physicalId, resourceType, properties, previousProperties) {
@@ -59618,11 +59781,25 @@ var ACMCertificateProvider = class {
59618
59781
  * Throws on `VALIDATION_TIMED_OUT` / `FAILED` (terminal failures) and on
59619
59782
  * polling-cap exhaustion (treated as timeout). SIGINT short-circuits the
59620
59783
  * loop and returns control to the deploy engine's cleanup path.
59784
+ *
59785
+ * Both throws are `ProvisioningError`s carrying `certificateArn` as their
59786
+ * `physicalId` (issue
59787
+ * [#2169](https://github.com/go-to-k/cdkd/issues/2169)). They used to be
59788
+ * plain `Error`s whose only reference to the certificate was the message
59789
+ * TEXT, which is not something a caller can act on. The certificate is real
59790
+ * by the time either fires, so the error that reports the failure is the one
59791
+ * piece of the failure path that should still be able to name it.
59792
+ *
59793
+ * Says nothing about what happens to the certificate afterwards: the caller
59794
+ * owns that. `create()`'s catch retires it (see
59795
+ * {@link ACMCertificateProvider.cleanupRequestedCertificate}) and appends a
59796
+ * note to this message only when that cleanup could NOT.
59621
59797
  */
59622
- async waitForCertificateIssued(certificateArn, logicalId) {
59798
+ async waitForCertificateIssued(certificateArn, logicalId, resourceType) {
59623
59799
  this.logger.debug(`Waiting for ACM certificate ${certificateArn} to reach ISSUED status...`);
59624
59800
  let interrupted = false;
59625
- let validationOptionsLogged = false;
59801
+ /** The last validation-record block printed, so we re-print only on a CHANGE. */
59802
+ let lastValidationOptions;
59626
59803
  const sigintHandler = () => {
59627
59804
  interrupted = true;
59628
59805
  };
@@ -59640,17 +59817,20 @@ var ACMCertificateProvider = class {
59640
59817
  this.logger.debug(`ACM certificate ${certificateArn} is ISSUED`);
59641
59818
  return;
59642
59819
  }
59643
- if (status === "FAILED" || status === "VALIDATION_TIMED_OUT" || status === "INACTIVE" || status === "REVOKED" || status === "EXPIRED") throw new Error(`ACM certificate ${logicalId} (${certificateArn}) entered terminal status ${status} during validation. Check ACM console / DNS records to diagnose.`);
59644
- if (status === "PENDING_VALIDATION" && !validationOptionsLogged && validations.length > 0) {
59645
- this.logValidationOptions(validations);
59646
- validationOptionsLogged = true;
59820
+ if (status === "FAILED" || status === "VALIDATION_TIMED_OUT" || status === "INACTIVE" || status === "REVOKED" || status === "EXPIRED") throw new ProvisioningError(`ACM certificate ${logicalId} (${certificateArn}) entered terminal status ${status} during validation. Check ACM console / DNS records to diagnose.`, resourceType, logicalId, certificateArn);
59821
+ if (status === "PENDING_VALIDATION") {
59822
+ const rendered = this.renderValidationOptions(validations);
59823
+ if (rendered !== void 0 && rendered !== lastValidationOptions) {
59824
+ this.logger.info(rendered);
59825
+ lastValidationOptions = rendered;
59826
+ }
59647
59827
  }
59648
59828
  this.logger.debug(`ACM certificate ${certificateArn} status: ${status} (attempt ${attempt}/${this.maxPollAttempts})`);
59649
59829
  const sleepEnd = Date.now() + this.pollIntervalMs;
59650
59830
  const tickMs = Math.min(1e3, this.pollIntervalMs);
59651
59831
  while (Date.now() < sleepEnd && !interrupted) await new Promise((resolve) => setTimeout(resolve, tickMs));
59652
59832
  }
59653
- throw new Error(`ACM certificate ${logicalId} (${certificateArn}) did not reach ISSUED status within ${this.maxPollAttempts * this.pollIntervalMs / 1e3}s. If your DNS zone is manually managed, you may need to increase --resource-timeout AWS::CertificateManager::Certificate=<duration> or set CDKD_NO_WAIT=true.`);
59833
+ throw new ProvisioningError(`ACM certificate ${logicalId} (${certificateArn}) did not reach ISSUED status within ${this.maxPollAttempts * this.pollIntervalMs / 1e3}s. ` + (lastValidationOptions === void 0 ? "AWS had not published this certificate's validation records before the wait ran out, so none were printed — re-run the deploy to request them again. " : `Add the validation records printed above to your DNS zone and re-run the deploy. `) + "ACM reuses a domain's validation CNAME across certificates, so records you add now validate the next attempt. To wait LONGER, raise CDKD_ACM_POLL_ATTEMPTS / CDKD_ACM_POLL_INTERVAL_MS — this cap is the provider's OWN and is what just fired; --resource-timeout alone will not lengthen it. Past ~30m also raise --resource-timeout AWS::CertificateManager::Certificate=<duration>, or the engine's per-resource deadline becomes the shorter of the two. Or set CDKD_NO_WAIT=true to keep the certificate and validate it out of band.", resourceType, logicalId, certificateArn);
59654
59834
  } finally {
59655
59835
  process.removeListener("SIGINT", sigintHandler);
59656
59836
  }
@@ -59660,16 +59840,13 @@ var ACMCertificateProvider = class {
59660
59840
  * user can copy / paste them into Route 53 / Cloudflare / etc. while the
59661
59841
  * cert is still PENDING_VALIDATION.
59662
59842
  */
59663
- logValidationOptions(validations) {
59843
+ renderValidationOptions(validations) {
59664
59844
  const lines = ["ACM certificate is PENDING_VALIDATION. Add the following DNS records to validate:"];
59665
59845
  for (const v of validations) if (v.ValidationMethod === "DNS" && v.ResourceRecord) {
59666
59846
  const r = v.ResourceRecord;
59667
59847
  lines.push(` ${v.DomainName} — ${r.Type} ${r.Name} -> ${r.Value}`);
59668
- } else if (v.ValidationMethod === "EMAIL") {
59669
- const emails = (v.ValidationEmails ?? []).join(", ");
59670
- lines.push(` ${v.DomainName} — confirmation email sent to: ${emails || "<none>"}`);
59671
- }
59672
- this.logger.info(lines.join("\n"));
59848
+ } else if (v.ValidationMethod === "EMAIL" && (v.ValidationEmails ?? []).length > 0) lines.push(` ${v.DomainName} — confirmation email sent to: ${v.ValidationEmails.join(", ")}`);
59849
+ return lines.length === 1 ? void 0 : lines.join("\n");
59673
59850
  }
59674
59851
  async updateTags(certificateArn, newTags, oldTags) {
59675
59852
  const newTagMap = new Map((newTags || []).map((t) => [t.Key, t.Value]));
@@ -86442,7 +86619,7 @@ function createMigrateCommand() {
86442
86619
  */
86443
86620
  function buildProgram() {
86444
86621
  const program = new Command();
86445
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.39");
86622
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.41");
86446
86623
  program.hook("preAction", (_thisCommand, actionCommand) => {
86447
86624
  const { profile } = actionCommand.optsWithGlobals();
86448
86625
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;