@go-to-k/cdkd 0.284.39 → 0.284.40

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-B3MQFW1K.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-BeKDzUe9.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";
@@ -23919,9 +23919,10 @@ const generations = /* @__PURE__ */ new Map();
23919
23919
  * long-lived process cannot grow the map without bound.
23920
23920
  */
23921
23921
  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);
23922
+ const tokenKey = (scope, logicalId, maxLength, charset) => `${scope}\u0000${process.env["AWS_REGION"] ?? ""}\u0000${getCurrentStackName() ?? ""}\u0000${logicalId}\u0000${maxLength}\u0000${charset}`;
23923
+ const derive = (key, generation, maxLength, charset) => {
23924
+ const digest = createHash("sha256").update(`${PROCESS_NONCE}\u0000${key}\u0000${generation}`).digest("hex");
23925
+ return `cdkd${charset === "alphanumeric" ? "" : "-"}${digest}`.slice(0, maxLength);
23925
23926
  };
23926
23927
  /**
23927
23928
  * Get the idempotency token for one logical create, minting it on first use and
@@ -23935,10 +23936,10 @@ const derive = (key, generation, maxLength) => {
23935
23936
  * ```
23936
23937
  */
23937
23938
  function acquireIdempotencyToken(options) {
23938
- const { scope, logicalId, maxLength = 64 } = options;
23939
- const key = tokenKey(scope, logicalId);
23939
+ const { scope, logicalId, maxLength = 64, charset = "default" } = options;
23940
+ const key = tokenKey(scope, logicalId, maxLength, charset);
23940
23941
  const existing = inFlight.get(key);
23941
- const value = existing ?? derive(key, generations.get(key) ?? 0, maxLength);
23942
+ const value = existing ?? derive(key, generations.get(key) ?? 0, maxLength, charset);
23942
23943
  if (existing === void 0) inFlight.set(key, value);
23943
23944
  return {
23944
23945
  value,
@@ -59380,6 +59381,19 @@ function isCertificateInUseError(error) {
59380
59381
  * consumers (CloudFront, ALB) will fail to start if they reach the cert
59381
59382
  * before it issues, but that's the documented trade-off.
59382
59383
  *
59384
+ * **A create that fails after requesting DELETES its certificate** (issue
59385
+ * [#2169](https://github.com/go-to-k/cdkd/issues/2169)). `RequestCertificate`
59386
+ * materializes the certificate immediately and everything after it can fail —
59387
+ * most commonly the ISSUED wait, which a DNS-validated certificate whose
59388
+ * validation records are not live yet exhausts by construction. Before this,
59389
+ * the ARN was thrown away with the error: the certificate existed in AWS with
59390
+ * nothing naming it (absent from state, so invisible to `cdkd state show` and
59391
+ * unreachable by `cdkd destroy`), and the next `cdkd deploy` requested another
59392
+ * one — an orphan per attempt. `create()` now retires the certificate it just
59393
+ * requested before re-throwing, so a failed deploy leaves nothing behind and a
59394
+ * retry starts clean. See {@link ACMCertificateProvider.cleanupRequestedCertificate}
59395
+ * for why deleting is safe and why it is preferred over recording the remnant.
59396
+ *
59383
59397
  * **CloudFront cross-region note**: ACM certificates referenced by a
59384
59398
  * CloudFront Distribution MUST live in `us-east-1`. cdkd does not enforce
59385
59399
  * this — it's the developer's responsibility to deploy the certificate
@@ -59429,12 +59443,22 @@ var ACMCertificateProvider = class {
59429
59443
  if (Object.keys(options).length > 0) input["Options"] = options;
59430
59444
  const tags = properties["Tags"];
59431
59445
  if (tags && Array.isArray(tags) && tags.length > 0) input["Tags"] = tags;
59446
+ const idempotencyToken = acquireIdempotencyToken({
59447
+ scope: "RequestCertificate",
59448
+ logicalId,
59449
+ maxLength: 32,
59450
+ charset: "alphanumeric"
59451
+ });
59452
+ input["IdempotencyToken"] = idempotencyToken.value;
59453
+ let requestedArn;
59432
59454
  try {
59433
59455
  const certificateArn = (await this.acmClient.send(new RequestCertificateCommand(input))).CertificateArn;
59434
59456
  if (!certificateArn) throw new ProvisioningError(`RequestCertificate succeeded but no CertificateArn returned for ${logicalId}`, resourceType, logicalId);
59457
+ requestedArn = certificateArn;
59435
59458
  this.logger.debug(`Requested ACM certificate: ${certificateArn}`);
59436
- if (!(process.env["CDKD_NO_WAIT"] === "true")) await this.waitForCertificateIssued(certificateArn, logicalId);
59459
+ if (!(process.env["CDKD_NO_WAIT"] === "true")) await this.waitForCertificateIssued(certificateArn, logicalId, resourceType);
59437
59460
  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.`);
59461
+ idempotencyToken.release();
59438
59462
  return {
59439
59463
  physicalId: certificateArn,
59440
59464
  attributes: {
@@ -59443,9 +59467,84 @@ var ACMCertificateProvider = class {
59443
59467
  }
59444
59468
  };
59445
59469
  } catch (error) {
59446
- if (error instanceof CdkdError) throw error;
59470
+ const survivorNote = requestedArn ? await this.cleanupRequestedCertificate(requestedArn, logicalId, idempotencyToken) : void 0;
59471
+ if (error instanceof CdkdError) {
59472
+ if (survivorNote === void 0) throw error;
59473
+ throw new ProvisioningError(`${error.message} ${survivorNote}`, resourceType, logicalId, requestedArn, error);
59474
+ }
59447
59475
  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);
59476
+ throw new ProvisioningError(`Failed to create ACM certificate ${logicalId}: ${error instanceof Error ? error.message : String(error)}` + (survivorNote === void 0 ? "" : ` ${survivorNote}`), resourceType, logicalId, requestedArn, cause);
59477
+ }
59478
+ }
59479
+ /**
59480
+ * Retire the certificate a FAILED `create()` just requested, so the failure
59481
+ * leaves nothing behind (issue
59482
+ * [#2169](https://github.com/go-to-k/cdkd/issues/2169)).
59483
+ *
59484
+ * Returns `undefined` when the certificate is gone, or a one-line note naming
59485
+ * the survivor when it could not be deleted. Never throws: the original
59486
+ * create error is what the user needs to see, and a cleanup failure must
59487
+ * degrade that message rather than replace it.
59488
+ *
59489
+ * ## Why DELETING is right, when the obvious objection is that it throws away
59490
+ * the user's DNS work
59491
+ *
59492
+ * It does not. ACM derives a domain's validation CNAME from the domain and
59493
+ * the account, not from the certificate, and documents that the record "must
59494
+ * be added to your DNS database only once": *"Without the need to repeat
59495
+ * validation, you can request additional ACM certificates for your fully
59496
+ * qualified domain name (FQDN) for as long as the CNAME record remains in
59497
+ * place ... You can also replace a deleted certificate."*
59498
+ * (https://docs.aws.amazon.com/acm/latest/userguide/dns-validation.html)
59499
+ * So the CNAMEs a user adds after seeing this failure validate the NEXT
59500
+ * certificate too, and the retry issues normally. The intuition that a
59501
+ * `PENDING_VALIDATION` certificate is precious does not hold for ACM.
59502
+ *
59503
+ * ## Why not RECORD the remnant in cdkd state instead
59504
+ *
59505
+ * That was this fix's first shape, and it does not work. A state record makes
59506
+ * the next deploy diff the resource as `NO_CHANGE` -- the recorded properties
59507
+ * ARE the template's -- so cdkd would never touch that certificate again:
59508
+ * `cdkd deploy` would print "No changes detected" and exit 0 over a
59509
+ * certificate that is still unusable, and the CloudFront / ALB consuming it
59510
+ * would fail with no explanation. Turning a loud failure into a silent one is
59511
+ * worse than the orphan it fixes. Making the diff re-provision it instead
59512
+ * needs a marker on the state record -- a schema bump -- to buy behaviour
59513
+ * that deleting gives for free.
59514
+ *
59515
+ * ## The "keep it" escape hatch already exists
59516
+ *
59517
+ * A user who WANTS the certificate to outlive the deploy has
59518
+ * `CDKD_NO_WAIT=true` (`cdkd deploy --no-wait`), which returns success with
59519
+ * the certificate recorded in state and never reaches this path. "Wait for
59520
+ * it" and "keep it even if the wait fails" are different requests, and cdkd
59521
+ * already has a flag for the second.
59522
+ */
59523
+ /**
59524
+ * The region an ACM ARN names, for the manual retire command. Taken from the
59525
+ * ARN rather than from the client so the command a user pastes matches the
59526
+ * certificate it names even when their shell default differs. Falls back to
59527
+ * the literal placeholder for a shape this cannot parse -- a wrong region
59528
+ * would make the command silently address a different account/region.
59529
+ */
59530
+ regionOfArn(arn) {
59531
+ return arn.split(":")[3] || "<region>";
59532
+ }
59533
+ async cleanupRequestedCertificate(certificateArn, logicalId, idempotencyToken) {
59534
+ try {
59535
+ await this.acmClient.send(new DeleteCertificateCommand({ CertificateArn: certificateArn }));
59536
+ 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.`);
59537
+ idempotencyToken.release();
59538
+ return;
59539
+ } catch (cleanupError) {
59540
+ if (cleanupError instanceof ResourceNotFoundException$7 || cleanupError instanceof Error && cleanupError.name === "ResourceNotFoundException") {
59541
+ this.logger.info(`The ACM certificate ${certificateArn} that the failed create of ${logicalId} requested was already gone; nothing to clean up.`);
59542
+ idempotencyToken.release();
59543
+ return;
59544
+ }
59545
+ const detail = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
59546
+ this.logger.warn(`Could not delete the ACM certificate ${certificateArn} that the failed create of ${logicalId} requested: ${detail}`);
59547
+ 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
59548
  }
59450
59549
  }
59451
59550
  async update(logicalId, physicalId, resourceType, properties, previousProperties) {
@@ -59618,11 +59717,25 @@ var ACMCertificateProvider = class {
59618
59717
  * Throws on `VALIDATION_TIMED_OUT` / `FAILED` (terminal failures) and on
59619
59718
  * polling-cap exhaustion (treated as timeout). SIGINT short-circuits the
59620
59719
  * loop and returns control to the deploy engine's cleanup path.
59720
+ *
59721
+ * Both throws are `ProvisioningError`s carrying `certificateArn` as their
59722
+ * `physicalId` (issue
59723
+ * [#2169](https://github.com/go-to-k/cdkd/issues/2169)). They used to be
59724
+ * plain `Error`s whose only reference to the certificate was the message
59725
+ * TEXT, which is not something a caller can act on. The certificate is real
59726
+ * by the time either fires, so the error that reports the failure is the one
59727
+ * piece of the failure path that should still be able to name it.
59728
+ *
59729
+ * Says nothing about what happens to the certificate afterwards: the caller
59730
+ * owns that. `create()`'s catch retires it (see
59731
+ * {@link ACMCertificateProvider.cleanupRequestedCertificate}) and appends a
59732
+ * note to this message only when that cleanup could NOT.
59621
59733
  */
59622
- async waitForCertificateIssued(certificateArn, logicalId) {
59734
+ async waitForCertificateIssued(certificateArn, logicalId, resourceType) {
59623
59735
  this.logger.debug(`Waiting for ACM certificate ${certificateArn} to reach ISSUED status...`);
59624
59736
  let interrupted = false;
59625
- let validationOptionsLogged = false;
59737
+ /** The last validation-record block printed, so we re-print only on a CHANGE. */
59738
+ let lastValidationOptions;
59626
59739
  const sigintHandler = () => {
59627
59740
  interrupted = true;
59628
59741
  };
@@ -59640,17 +59753,20 @@ var ACMCertificateProvider = class {
59640
59753
  this.logger.debug(`ACM certificate ${certificateArn} is ISSUED`);
59641
59754
  return;
59642
59755
  }
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;
59756
+ 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);
59757
+ if (status === "PENDING_VALIDATION") {
59758
+ const rendered = this.renderValidationOptions(validations);
59759
+ if (rendered !== void 0 && rendered !== lastValidationOptions) {
59760
+ this.logger.info(rendered);
59761
+ lastValidationOptions = rendered;
59762
+ }
59647
59763
  }
59648
59764
  this.logger.debug(`ACM certificate ${certificateArn} status: ${status} (attempt ${attempt}/${this.maxPollAttempts})`);
59649
59765
  const sleepEnd = Date.now() + this.pollIntervalMs;
59650
59766
  const tickMs = Math.min(1e3, this.pollIntervalMs);
59651
59767
  while (Date.now() < sleepEnd && !interrupted) await new Promise((resolve) => setTimeout(resolve, tickMs));
59652
59768
  }
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.`);
59769
+ 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
59770
  } finally {
59655
59771
  process.removeListener("SIGINT", sigintHandler);
59656
59772
  }
@@ -59660,16 +59776,13 @@ var ACMCertificateProvider = class {
59660
59776
  * user can copy / paste them into Route 53 / Cloudflare / etc. while the
59661
59777
  * cert is still PENDING_VALIDATION.
59662
59778
  */
59663
- logValidationOptions(validations) {
59779
+ renderValidationOptions(validations) {
59664
59780
  const lines = ["ACM certificate is PENDING_VALIDATION. Add the following DNS records to validate:"];
59665
59781
  for (const v of validations) if (v.ValidationMethod === "DNS" && v.ResourceRecord) {
59666
59782
  const r = v.ResourceRecord;
59667
59783
  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"));
59784
+ } else if (v.ValidationMethod === "EMAIL" && (v.ValidationEmails ?? []).length > 0) lines.push(` ${v.DomainName} — confirmation email sent to: ${v.ValidationEmails.join(", ")}`);
59785
+ return lines.length === 1 ? void 0 : lines.join("\n");
59673
59786
  }
59674
59787
  async updateTags(certificateArn, newTags, oldTags) {
59675
59788
  const newTagMap = new Map((newTags || []).map((t) => [t.Key, t.Value]));
@@ -86442,7 +86555,7 @@ function createMigrateCommand() {
86442
86555
  */
86443
86556
  function buildProgram() {
86444
86557
  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");
86558
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.40");
86446
86559
  program.hook("preAction", (_thisCommand, actionCommand) => {
86447
86560
  const { profile } = actionCommand.optsWithGlobals();
86448
86561
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;