@go-to-k/cdkd 0.280.43 → 0.280.45

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 requireConfigArray, $t as expectedOwnerParam, A as green, An as withErrorHandling, At as getDockerCmd, B as slowCcOperationTimeoutMs, Bt as resolveAutoAssetStorage, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as StackHasActiveImportsError, Ct as ensureAssetStorage, D as bold, Dt as validateContainerRepoName, E as formatResourceLine, Et as validateAssetBucketName, F as clearOnUpdateRemoval, Ft as Synthesizer, G as refStateLookupFromResource, Gt as resolveUseCdkBootstrapAssets, H as isTerminationProtectionPropagationError, Ht as resolveSkipPrefix, I as ProviderRegistry, It as synthesisStatusMessage, J as resolveExplicitPhysicalId, Jt as CFN_TEMPLATE_BODY_LIMIT, K as WAFv2WebACLProvider, Kt as stateBucketExistenceConfirmed, L as findActionableSilentDrops, Lt as getDefaultStateBucketName, M as yellow, Mt as runDockerStreaming, N as IAMRoleProvider, Nt as AssetManifestLoader, O as cyan, Ot as buildDockerImage, P as collectInlinePolicyNamesManagedBySiblings, Pt as getDockerImageBySourceHash, Q as replayWarn, Qt as uploadCfnTemplate, R as findSilentDropProperties, Rt as getLegacyStateBucketName, S as extractDeploymentEventError, Sn as ResourceUpdateNotSupportedError, St as BOOTSTRAP_MARKER_PREFIX, T as renderStatefulReason, Tt as parseBootstrapMarker, U as IntrinsicFunctionResolver, Ut as resolveStateBucketWithDefault, V as disableInstanceApiTermination, Vt as resolveCaptureObservedState, W as cfnRefValueFromPhysicalId, Wt as resolveStateBucketWithDefaultAndSource, X as configStringRefusal, Xt as MIGRATE_TMP_PREFIX, Y as assertRegionMatch, Yt as CFN_TEMPLATE_URL_LIMIT, Z as readConfigString, Zt as findLargeInlineResources, _ as createPreDeleteFinalSnapshot, _n as MissingCdkCliError, _t as buildAssetRedirectMap, a as DeploymentEventsStore, an as getAwsClients, at as withRetry, b as unsupportedFinalSnapshotError, bn as ProvisioningError, bt as rewriteTemplateAssetReferences, c as replayFailedOperations, ct as DagBuilder, d as IMPLICIT_DELETE_DEPENDENCIES, dt as S3StateBackend, en as AssemblyReader, et as requireConfigObject, f as computeImplicitDeleteEdges, fn as DeployCancelledError, ft as rebuildClientForBucketRegion, g as ccRoutedFinalSnapshotError, gt as WorkGraph, h as buildFinalSnapshotIdentifier, hn as LocalStartServiceError, ht as stringifyValue, i as DeploymentEventsReader, in as AwsClients, it as describeTypeWithThrottleRetry, j as red, jt as runDockerForeground, k as gray, kn as normalizeAwsError, kt as formatDockerLoginError, l as replayRollback, ln as CdkdError, lt as TemplateParser, m as PRE_DELETE_SNAPSHOT_TYPES, mn as LocalMigrateError, mt as AssetPublisher, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as applyRoleArnIfSet, o as planFailedOps, on as resetAwsClients, ot as isRetryableTransientError, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as LocalInvokeBuildError$1, pt as shouldRetainResource, q as normalizeAwsTagsToCfn, qt as warnDeprecatedNoPrefixCliFlag, r as DeployEngine, rn as resolveBucketRegion, rt as DiffCalculator, s as planRollback, sn as setAwsClients, st as isThrottlingError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as processStackMessages, tt as requireConfigString, u as withResourceDeadline, ut as LockManager, v as isFinalSnapshotError, vn as NestedStackChildDirectDestroyError, vt as createAssetRedirectResolver, w as isStatefulRecreateTargetSync, wn as StackTerminationProtectionError, wt as getBootstrapMarkerKey, x as makeCanonicalizePropertiesFn, xn as ResourceTimeoutError, xt as AssetModeResolver, y as refusesFinalSnapshot, yn as PartialFailureError, yt as loadPublishableAssetManifest, z as CloudControlProvider, zt as resolveApp } from "./deploy-engine-92FflEWX.js";
2
+ import { $ as requireConfigArray, $t as expectedOwnerParam, A as green, An as withErrorHandling, At as getDockerCmd, B as slowCcOperationTimeoutMs, Bt as resolveAutoAssetStorage, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as StackHasActiveImportsError, Ct as ensureAssetStorage, D as bold, Dt as validateContainerRepoName, E as formatResourceLine, Et as validateAssetBucketName, F as clearOnUpdateRemoval, Ft as Synthesizer, G as refStateLookupFromResource, Gt as resolveUseCdkBootstrapAssets, H as isTerminationProtectionPropagationError, Ht as resolveSkipPrefix, I as ProviderRegistry, It as synthesisStatusMessage, J as resolveExplicitPhysicalId, Jt as CFN_TEMPLATE_BODY_LIMIT, K as WAFv2WebACLProvider, Kt as stateBucketExistenceConfirmed, L as findActionableSilentDrops, Lt as getDefaultStateBucketName, M as yellow, Mt as runDockerStreaming, N as IAMRoleProvider, Nt as AssetManifestLoader, O as cyan, Ot as buildDockerImage, P as collectInlinePolicyNamesManagedBySiblings, Pt as getDockerImageBySourceHash, Q as replayWarn, Qt as uploadCfnTemplate, R as findSilentDropProperties, Rt as getLegacyStateBucketName, S as extractDeploymentEventError, Sn as ResourceUpdateNotSupportedError, St as BOOTSTRAP_MARKER_PREFIX, T as renderStatefulReason, Tt as parseBootstrapMarker, U as IntrinsicFunctionResolver, Ut as resolveStateBucketWithDefault, V as disableInstanceApiTermination, Vt as resolveCaptureObservedState, W as cfnRefValueFromPhysicalId, Wt as resolveStateBucketWithDefaultAndSource, X as configStringRefusal, Xt as MIGRATE_TMP_PREFIX, Y as assertRegionMatch, Yt as CFN_TEMPLATE_URL_LIMIT, Z as readConfigString, Zt as findLargeInlineResources, _ as createPreDeleteFinalSnapshot, _n as MissingCdkCliError, _t as buildAssetRedirectMap, a as DeploymentEventsStore, an as getAwsClients, at as withRetry, b as unsupportedFinalSnapshotError, bn as ProvisioningError, bt as rewriteTemplateAssetReferences, c as replayFailedOperations, ct as DagBuilder, d as IMPLICIT_DELETE_DEPENDENCIES, dt as S3StateBackend, en as AssemblyReader, et as requireConfigObject, f as computeImplicitDeleteEdges, fn as DeployCancelledError, ft as rebuildClientForBucketRegion, g as ccRoutedFinalSnapshotError, gt as WorkGraph, h as buildFinalSnapshotIdentifier, hn as LocalStartServiceError, ht as stringifyValue, i as DeploymentEventsReader, in as AwsClients, it as describeTypeWithThrottleRetry, j as red, jt as runDockerForeground, k as gray, kn as normalizeAwsError, kt as formatDockerLoginError, l as replayRollback, ln as CdkdError, lt as TemplateParser, m as PRE_DELETE_SNAPSHOT_TYPES, mn as LocalMigrateError, mt as AssetPublisher, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as applyRoleArnIfSet, o as planFailedOps, on as resetAwsClients, ot as isRetryableTransientError, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as LocalInvokeBuildError$1, pt as shouldRetainResource, q as normalizeAwsTagsToCfn, qt as warnDeprecatedNoPrefixCliFlag, r as DeployEngine, rn as resolveBucketRegion, rt as DiffCalculator, s as planRollback, sn as setAwsClients, st as isThrottlingError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as processStackMessages, tt as requireConfigString, u as withResourceDeadline, ut as LockManager, v as isFinalSnapshotError, vn as NestedStackChildDirectDestroyError, vt as createAssetRedirectResolver, w as isStatefulRecreateTargetSync, wn as StackTerminationProtectionError, wt as getBootstrapMarkerKey, x as makeCanonicalizePropertiesFn, xn as ResourceTimeoutError, xt as AssetModeResolver, y as refusesFinalSnapshot, yn as PartialFailureError, yt as loadPublishableAssetManifest, z as CloudControlProvider, zt as resolveApp } from "./deploy-engine-Dnfehcah.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
4
- import { t as ASGProvider } from "./asg-provider-DhDS2HGs.js";
4
+ import { t as ASGProvider } from "./asg-provider-DwyF44v8.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";
@@ -33013,6 +33013,102 @@ function isZoneDisabledForMutationError(error) {
33013
33013
  return error instanceof Error && error.message.includes("is marked disabled for mutation");
33014
33014
  }
33015
33015
  /**
33016
+ * A `HostedZoneName` that resolved cleanly but matches no zone.
33017
+ *
33018
+ * Distinct from "no zone was specified": the DELETE path treats a vanished
33019
+ * zone as already-gone (the zone's records went with it) rather than surfacing
33020
+ * a misconfiguration error that would block `cdkd destroy`.
33021
+ */
33022
+ var HostedZoneNameNotFoundError = class HostedZoneNameNotFoundError extends ProvisioningError {
33023
+ constructor(message, resourceType, logicalId, physicalId, cause) {
33024
+ super(message, resourceType, logicalId, physicalId, cause);
33025
+ this.name = "HostedZoneNameNotFoundError";
33026
+ Object.setPrototypeOf(this, HostedZoneNameNotFoundError.prototype);
33027
+ }
33028
+ };
33029
+ /** Segment count of cdkd's `AWS::Route53::RecordSet` composite physicalId. */
33030
+ const RECORD_SET_COMPOSITE_SEGMENTS = 3;
33031
+ /**
33032
+ * Parse cdkd's composite record-set physicalId `<hostedZoneId>|<Name>|<Type>`.
33033
+ *
33034
+ * Returns `undefined` for anything else — most importantly CloudFormation's
33035
+ * OWN physicalId for this type, which is the record name alone (`Ref` on an
33036
+ * `AWS::Route53::RecordSet` returns the domain name, and a DNS name can never
33037
+ * contain `|`). `cdkd import` stored that scalar verbatim before issue #1658,
33038
+ * so state files in the wild carry BOTH shapes and every consumer has to know
33039
+ * which one it is holding.
33040
+ */
33041
+ function parseRecordSetCompositeId(physicalId) {
33042
+ const parts = physicalId.split("|");
33043
+ if (parts.length !== RECORD_SET_COMPOSITE_SEGMENTS) return void 0;
33044
+ const [hostedZoneId, name, type] = parts;
33045
+ if (!hostedZoneId || !name || !type) return void 0;
33046
+ return {
33047
+ hostedZoneId,
33048
+ name,
33049
+ type
33050
+ };
33051
+ }
33052
+ /**
33053
+ * Octal escapes that must NOT be decoded, because decoding them is not
33054
+ * injective — two genuinely different names would compare EQUAL:
33055
+ *
33056
+ * - `056` is a period INSIDE a label. Route 53 reports the single-label
33057
+ * record `a.b` (period escaped) as `a\\056b.example.com.`, which decodes to
33058
+ * `a.b.example.com.` — identical to the different, two-label record
33059
+ * `a.b.example.com`.
33060
+ * - `134` is a literal backslash, which would re-introduce escape ambiguity.
33061
+ *
33062
+ * A false MATCH is far worse than a false miss here: the compare drives which
33063
+ * record `readCurrentState` reports and which hosted zone `create` picks, so
33064
+ * collapsing two records would target the wrong one.
33065
+ */
33066
+ const UNDECODABLE_OCTAL_ESCAPES = /* @__PURE__ */ new Set(["056", "134"]);
33067
+ /**
33068
+ * Canonicalize a record name for comparison.
33069
+ *
33070
+ * Two AWS-side spellings have to be absorbed, and missing either one makes
33071
+ * `readCurrentState` return `undefined` — which reads as "drift unknown"
33072
+ * rather than as an error:
33073
+ *
33074
+ * - Route 53 reports every name fully qualified with a TRAILING DOT, while a
33075
+ * template may declare it either way.
33076
+ * - Special characters come back OCTAL-ESCAPED: `*.example.com` is reported as
33077
+ * `\\052.example.com.`. That affects every wildcard record, including ones
33078
+ * cdkd created itself, so decoding it here fixes drift for the whole
33079
+ * population rather than only the imported ones.
33080
+ *
33081
+ * Known bound: Route 53 escapes per BYTE, so a non-ASCII name decodes to
33082
+ * latin-1 mojibake and will not match its template spelling. That is a false
33083
+ * MISS (drift unknown), never a false match, and CDK emits punycode for
33084
+ * internationalized names in practice.
33085
+ */
33086
+ function normalizeRecordName(name) {
33087
+ const lowered = name.replace(/\\([0-7]{3})/g, (match, oct) => UNDECODABLE_OCTAL_ESCAPES.has(oct) ? match : String.fromCharCode(parseInt(oct, 8))).toLowerCase();
33088
+ return lowered.endsWith(".") ? lowered : `${lowered}.`;
33089
+ }
33090
+ /**
33091
+ * Canonicalize a name for the QUERY side of a Route 53 list call.
33092
+ *
33093
+ * `normalizeRecordName` above is the COMPARE-side canonicalizer, and the two
33094
+ * are deliberately NOT the same function. Route 53 orders both hosted zones
33095
+ * and record sets by reversed labels of the name it STORES — lower-cased and
33096
+ * escape-ENCODED — and a `DNSName` / `StartRecordName` start key in any other
33097
+ * spelling positions the window somewhere else entirely. So the query side
33098
+ * must move TOWARD AWS's spelling (lower-case, encode `*`), where the compare
33099
+ * side moves AWS's answer toward the template's (decode escapes).
33100
+ *
33101
+ * Decoding here would be actively wrong: `*.example.com` sorts at `*` (0x2A)
33102
+ * while AWS stores `\052.example.com` and sorts it at `\` (0x5C), so every
33103
+ * name whose first label begins with `-` / `+` / a digit / an upper-case
33104
+ * letter falls BETWEEN them — with a bounded page, the wildcard record we
33105
+ * asked for is skipped straight past.
33106
+ */
33107
+ function canonicalizeQueryName(name) {
33108
+ const lowered = name.toLowerCase().replaceAll("*", "\\052");
33109
+ return lowered.endsWith(".") ? lowered : `${lowered}.`;
33110
+ }
33111
+ /**
33016
33112
  * AWS Route 53 Provider
33017
33113
  *
33018
33114
  * Implements resource provisioning for Route 53 resources:
@@ -33351,10 +33447,20 @@ var Route53Provider = class {
33351
33447
  }
33352
33448
  async deleteRecordSet(logicalId, physicalId, resourceType, properties, context) {
33353
33449
  this.logger.debug(`Deleting Route 53 record set ${logicalId}: ${physicalId}`);
33354
- const parts = physicalId.split("|");
33355
- if (parts.length !== 3) throw new ProvisioningError(`Invalid record set physical ID format: ${physicalId}`, resourceType, logicalId, physicalId);
33356
- const [hostedZoneId] = parts;
33357
33450
  if (!properties) throw new ProvisioningError(`Properties required to delete record set ${logicalId}`, resourceType, logicalId, physicalId);
33451
+ const composite = parseRecordSetCompositeId(physicalId);
33452
+ let hostedZoneId;
33453
+ if (composite) hostedZoneId = composite.hostedZoneId;
33454
+ else try {
33455
+ hostedZoneId = await this.resolveHostedZoneId(properties, logicalId, resourceType, physicalId, { requireUnambiguousZoneName: true });
33456
+ } catch (error) {
33457
+ if (error instanceof HostedZoneNameNotFoundError) {
33458
+ assertRegionMatch(await this.getClient().config.region(), context?.expectedRegion, resourceType, logicalId, physicalId);
33459
+ this.logger.warn(`Hosted zone "${String(properties?.["HostedZoneName"])}" for record set ${logicalId} (${physicalId}) no longer exists; treating the record as already deleted and dropping it from state.`);
33460
+ return;
33461
+ }
33462
+ throw error;
33463
+ }
33358
33464
  try {
33359
33465
  const resourceRecordSet = this.buildResourceRecordSet(properties);
33360
33466
  const sendDelete = () => this.getClient().send(new ChangeResourceRecordSetsCommand({
@@ -33702,22 +33808,65 @@ var Route53Provider = class {
33702
33808
  * If HostedZoneId is provided, use it directly.
33703
33809
  * If HostedZoneName is provided, resolve it to a HostedZoneId via ListHostedZonesByName.
33704
33810
  */
33705
- async resolveHostedZoneId(properties, logicalId, resourceType, physicalId) {
33811
+ async resolveHostedZoneId(properties, logicalId, resourceType, physicalId, options) {
33706
33812
  const hostedZoneId = properties["HostedZoneId"];
33707
- if (hostedZoneId) return hostedZoneId;
33813
+ if (typeof hostedZoneId === "string" && hostedZoneId) return hostedZoneId;
33814
+ let lookupErrorMessage;
33815
+ let lookupErrorCause;
33708
33816
  const hostedZoneName = properties["HostedZoneName"];
33709
- if (hostedZoneName) try {
33710
- const zones = (await this.getClient().send(new ListHostedZonesByNameCommand({
33711
- DNSName: hostedZoneName,
33712
- MaxItems: 1
33713
- }))).HostedZones ?? [];
33714
- const normalizedName = hostedZoneName.endsWith(".") ? hostedZoneName : `${hostedZoneName}.`;
33715
- const matchedZone = zones.find((z) => z.Name === normalizedName);
33817
+ if (typeof hostedZoneName === "string" && hostedZoneName) {
33818
+ let matched = [];
33819
+ let absenceIsSound = false;
33820
+ try {
33821
+ const response = await this.getClient().send(new ListHostedZonesByNameCommand({
33822
+ DNSName: canonicalizeQueryName(hostedZoneName),
33823
+ MaxItems: 5
33824
+ }));
33825
+ const zones = response.HostedZones ?? [];
33826
+ const normalizedName = normalizeRecordName(hostedZoneName);
33827
+ matched = zones.filter((z) => normalizeRecordName(z.Name ?? "") === normalizedName);
33828
+ absenceIsSound = !hostedZoneName.includes("\\") && (zones.length > 0 || response.IsTruncated !== true);
33829
+ } catch (error) {
33830
+ lookupErrorMessage = error instanceof Error ? error.message : String(error);
33831
+ lookupErrorCause = error instanceof Error ? error : void 0;
33832
+ this.logger.warn(`Failed to resolve HostedZoneName "${hostedZoneName}" for ${logicalId}: ${lookupErrorMessage}`);
33833
+ }
33834
+ if (matched.length > 1 && options?.requireUnambiguousZoneName) throw new ProvisioningError(`HostedZoneName "${hostedZoneName}" matches ${matched.length} hosted zones for ${logicalId} (split-horizon public/private DNS). Refusing to guess which one this record belongs to — set HostedZoneId explicitly, or pass the '<zoneId>|<name>|<type>' composite via --resource.`, resourceType, logicalId, physicalId);
33835
+ const matchedZone = matched[0];
33716
33836
  if (matchedZone?.Id) return matchedZone.Id.replace("/hostedzone/", "");
33717
- } catch (error) {
33718
- this.logger.warn(`Failed to resolve HostedZoneName "${hostedZoneName}" for ${logicalId}: ${error instanceof Error ? error.message : String(error)}`);
33837
+ if (absenceIsSound) throw new HostedZoneNameNotFoundError(`HostedZoneName "${hostedZoneName}" matches no hosted zone in this account/region for ${logicalId}`, resourceType, logicalId, physicalId);
33838
+ }
33839
+ throw new ProvisioningError(lookupErrorMessage ? `Could not resolve HostedZoneName "${String(hostedZoneName)}" for record set ${logicalId}: ${lookupErrorMessage}` : `Either HostedZoneId or HostedZoneName is required for record set ${logicalId}`, resourceType, logicalId, physicalId, lookupErrorCause);
33840
+ }
33841
+ /**
33842
+ * Recover `(hostedZoneId, name, type)` for a record set from EITHER shape of
33843
+ * physicalId (issue #1658).
33844
+ *
33845
+ * cdkd's own composite carries all three. CloudFormation's physicalId is the
33846
+ * record name alone, so the zone has to be resolved from the recorded
33847
+ * properties — which is exactly what `createRecordSet` does, so the answer is
33848
+ * the same triple the composite would have held. Returns `undefined` (rather
33849
+ * than throwing) when the zone cannot be resolved, because both callers of
33850
+ * this helper treat "cannot determine" as "nothing to report" rather than as
33851
+ * a failure.
33852
+ */
33853
+ async resolveRecordSetIdentity(physicalId, properties, options) {
33854
+ const composite = parseRecordSetCompositeId(physicalId);
33855
+ if (composite) return composite;
33856
+ if (!properties) return void 0;
33857
+ const name = typeof properties["Name"] === "string" ? properties["Name"] : physicalId;
33858
+ const type = properties["Type"];
33859
+ if (!name || typeof type !== "string" || !type) return void 0;
33860
+ try {
33861
+ return {
33862
+ hostedZoneId: await this.resolveHostedZoneId(properties, "RecordSet", "AWS::Route53::RecordSet", physicalId, options),
33863
+ name,
33864
+ type
33865
+ };
33866
+ } catch (err) {
33867
+ this.logger.debug(`Cannot resolve record-set identity for '${physicalId}': ${err instanceof Error ? err.message : String(err)}`);
33868
+ return;
33719
33869
  }
33720
- throw new ProvisioningError(`Either HostedZoneId or HostedZoneName is required for record set ${logicalId}`, resourceType, logicalId, physicalId);
33721
33870
  }
33722
33871
  /**
33723
33872
  * Read the AWS-current Route 53 resource configuration in CFn-property shape.
@@ -33733,17 +33882,21 @@ var Route53Provider = class {
33733
33882
  * `CloudWatchLogsLogGroupArn` to match cdkd state).
33734
33883
  * - `RecordSet` → `ListResourceRecordSets` filtered to the exact
33735
33884
  * `(name, type)` pair from the composite physicalId
33736
- * (`{zoneId}|{name}|{type}`). Surfaces TTL, ResourceRecords (with
33885
+ * (`{zoneId}|{name}|{type}`), or for a state record carrying
33886
+ * CloudFormation's scalar physicalId, written by `cdkd import`
33887
+ * before issue #1658 — from the recorded properties, so drift is
33888
+ * reported for those records instead of silently skipped.
33889
+ * Surfaces TTL, ResourceRecords (with
33737
33890
  * `[{Value}]` -> string[] re-shape to match cdkd state), AliasTarget,
33738
33891
  * Weight, Region, Failover, MultiValueAnswer, HealthCheckId,
33739
33892
  * GeoLocation, GeoProximityLocation, CidrRoutingConfig, SetIdentifier.
33740
33893
  *
33741
33894
  * Returns `undefined` when the parent zone is gone (`NoSuchHostedZone`).
33742
33895
  */
33743
- async readCurrentState(physicalId, _logicalId, resourceType) {
33896
+ async readCurrentState(physicalId, _logicalId, resourceType, properties) {
33744
33897
  switch (resourceType) {
33745
33898
  case "AWS::Route53::HostedZone": return this.readHostedZone(physicalId);
33746
- case "AWS::Route53::RecordSet": return this.readRecordSet(physicalId);
33899
+ case "AWS::Route53::RecordSet": return this.readRecordSet(physicalId, properties);
33747
33900
  default: return;
33748
33901
  }
33749
33902
  }
@@ -33789,24 +33942,24 @@ var Route53Provider = class {
33789
33942
  }
33790
33943
  return result;
33791
33944
  }
33792
- async readRecordSet(physicalId) {
33793
- const parts = physicalId.split("|");
33794
- if (parts.length < 3) return void 0;
33795
- const [hostedZoneId, name, type] = parts;
33796
- if (!hostedZoneId || !name || !type) return void 0;
33945
+ async readRecordSet(physicalId, properties) {
33946
+ const identity = await this.resolveRecordSetIdentity(physicalId, properties);
33947
+ if (!identity) return void 0;
33948
+ const { hostedZoneId, name, type } = identity;
33797
33949
  let resp;
33798
33950
  try {
33799
33951
  resp = await this.getClient().send(new ListResourceRecordSetsCommand({
33800
33952
  HostedZoneId: hostedZoneId,
33801
- StartRecordName: name,
33953
+ StartRecordName: canonicalizeQueryName(name),
33802
33954
  StartRecordType: type,
33803
- MaxItems: 1
33955
+ MaxItems: 5
33804
33956
  }));
33805
33957
  } catch (err) {
33806
33958
  if (err instanceof Error && err.name === "NoSuchHostedZone") return void 0;
33807
33959
  throw err;
33808
33960
  }
33809
- const recordSet = resp.ResourceRecordSets?.find((r) => r.Name === name && r.Type === type);
33961
+ const wantedName = normalizeRecordName(name);
33962
+ const recordSet = resp.ResourceRecordSets?.find((r) => r.Name !== void 0 && normalizeRecordName(r.Name) === wantedName && r.Type === type);
33810
33963
  if (!recordSet) return void 0;
33811
33964
  const result = {
33812
33965
  HostedZoneId: hostedZoneId,
@@ -33861,23 +34014,89 @@ var Route53Provider = class {
33861
34014
  /**
33862
34015
  * Adopt an existing Route 53 resource into cdkd state.
33863
34016
  *
33864
- * Supported types: `AWS::Route53::HostedZone` (full tag-based
33865
- * lookup); `AWS::Route53::RecordSet` (override-only — RecordSets are
33866
- * not taggable, and the composite child-of-zone identity makes auto
33867
- * lookup impractical).
34017
+ * Supported types: `AWS::Route53::HostedZone` (override-only);
34018
+ * `AWS::Route53::RecordSet` (override-only too the supplied id is
34019
+ * CANONICALIZED rather than trusted, using the template's
34020
+ * `HostedZoneId` / `HostedZoneName` + `Name` + `Type` and verified
34021
+ * against AWS, but an import with no override still declines because
34022
+ * a RecordSet has no standalone identity for auto mode to key on).
33868
34023
  */
33869
34024
  async import(input) {
33870
34025
  switch (input.resourceType) {
33871
34026
  case "AWS::Route53::HostedZone": return this.importHostedZone(input);
33872
- case "AWS::Route53::RecordSet":
33873
- if (input.knownPhysicalId) return {
33874
- physicalId: input.knownPhysicalId,
33875
- attributes: {}
33876
- };
33877
- return null;
34027
+ case "AWS::Route53::RecordSet": return this.importRecordSet(input);
33878
34028
  default: return null;
33879
34029
  }
33880
34030
  }
34031
+ /**
34032
+ * Adopt an existing record set (issue #1658).
34033
+ *
34034
+ * cdkd's physicalId is the composite `<hostedZoneId>|<Name>|<Type>` that
34035
+ * `createRecordSet` returns, but CloudFormation's is the record NAME alone.
34036
+ * In auto / `--migrate-from-cloudformation` mode `knownPhysicalId` is NOT a
34037
+ * user choice — the overrides are pre-populated from the CFn stack's
34038
+ * `DescribeStackResources` — so storing it verbatim wrote an id this
34039
+ * provider's own `delete` could not parse, and the migrated stack became
34040
+ * undestroyable with the defect only surfacing after the CFn stack had
34041
+ * already been retired.
34042
+ *
34043
+ * So re-canonicalize instead, the way `AWS::EC2::EIP` already does: an
34044
+ * already-composite override passes through (the documented
34045
+ * `--resource 'Id=zone|name|type'` form), anything else is rebuilt from the
34046
+ * template properties and VERIFIED against AWS.
34047
+ *
34048
+ * **Canonicalization is BEST-EFFORT and never drops the row.** When the
34049
+ * identity cannot be resolved or verified, the supplied id is adopted
34050
+ * VERBATIM with a warning rather than returning `null`. Returning `null`
34051
+ * there would be strictly worse than the bug this fixes: `import.ts` only
34052
+ * aborts when ZERO resources import, so under
34053
+ * `--migrate-from-cloudformation` the run proceeds to `UpdateStack(Retain)`
34054
+ * + `DeleteStack` and the record ends up in NEITHER CloudFormation nor cdkd
34055
+ * state — after which the next deploy CREATEs over a live RRSet and fails.
34056
+ * Adopting verbatim is safe now precisely because `deleteRecordSet` and
34057
+ * `readRecordSet` both accept the scalar form (the other half of this fix).
34058
+ *
34059
+ * The ordinary shape that makes verification legitimately fail is a
34060
+ * `HostedZoneId` still carrying an unresolved intrinsic (`import.ts`
34061
+ * substitutes only single-key `{Ref}` before calling a provider). A
34062
+ * split-horizon zone name is refused for safety and lands here too.
34063
+ * (Wildcard records used to fail here as well; `normalizeRecordName` now
34064
+ * decodes Route 53's octal escapes, so they canonicalize normally.)
34065
+ *
34066
+ * This stays OVERRIDE-ONLY (see the "sub-resources without a standalone
34067
+ * identity" list in docs/import.md): the id is now derived from the template
34068
+ * rather than trusted, but an import with no override at all still declines,
34069
+ * because a RecordSet has no standalone identity for auto mode to key on.
34070
+ */
34071
+ async importRecordSet(input) {
34072
+ const known = input.knownPhysicalId;
34073
+ if (!known) return null;
34074
+ if (parseRecordSetCompositeId(known)) return {
34075
+ physicalId: known,
34076
+ attributes: {}
34077
+ };
34078
+ const adoptVerbatim = (reason) => {
34079
+ this.logger.warn(`Record set ${input.logicalId}: could not canonicalize physical id '${known}' (${reason}). Adopting it as-is — delete and drift both accept this form, but a later deploy that UPDATEs the record will rewrite it to cdkd's '<zoneId>|<name>|<type>' composite.`);
34080
+ return {
34081
+ physicalId: known,
34082
+ attributes: {}
34083
+ };
34084
+ };
34085
+ const identity = await this.resolveRecordSetIdentity(known, input.properties, { requireUnambiguousZoneName: true });
34086
+ if (!identity) return adoptVerbatim("the template does not unambiguously resolve to a hosted zone + Name + Type");
34087
+ const compositeId = `${identity.hostedZoneId}|${identity.name}|${identity.type}`;
34088
+ let observed;
34089
+ try {
34090
+ observed = await this.readRecordSet(compositeId);
34091
+ } catch (err) {
34092
+ return adoptVerbatim(`verification failed: ${err instanceof Error ? err.message : String(err)}`);
34093
+ }
34094
+ if (!observed) return adoptVerbatim("the record was not found under the resolved hosted zone");
34095
+ return {
34096
+ physicalId: compositeId,
34097
+ attributes: {}
34098
+ };
34099
+ }
33881
34100
  async importHostedZone(input) {
33882
34101
  if (input.knownPhysicalId) try {
33883
34102
  await this.getClient().send(new GetHostedZoneCommand({ Id: input.knownPhysicalId }));
@@ -71936,7 +72155,7 @@ function createMigrateCommand() {
71936
72155
  */
71937
72156
  function buildProgram() {
71938
72157
  const program = new Command();
71939
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.280.43");
72158
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.280.45");
71940
72159
  program.hook("preAction", (_thisCommand, actionCommand) => {
71941
72160
  const { profile } = actionCommand.optsWithGlobals();
71942
72161
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;