@go-to-k/cdkd 0.281.23 → 0.281.24

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 configStringRefusal, $t as MIGRATE_TMP_PREFIX, A as green, At as validateContainerRepoName, B as slowCcOperationTimeoutMs, Bt as getDefaultStateBucketName, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as PartialFailureError, Ct as rewriteTemplateAssetReferences, D as bold, Dn as StackHasActiveImportsError, Dt as getBootstrapMarkerKey, E as formatResourceLine, En as ResourceUpdateNotSupportedError, Et as ensureAssetStorage, F as clearOnUpdateRemoval, Ft as runDockerStreaming, G as getAccountInfo, Gt as resolveSkipPrefix, H as isTerminationProtectionPropagationError, Ht as resolveApp, I as ProviderRegistry, It as AssetManifestLoader, J as normalizeAwsTagsToCfn, Jt as resolveUseCdkBootstrapAssets, K as refStateLookupFromResource, Kt as resolveStateBucketWithDefault, L as findActionableSilentDrops, Lt as getDockerImageBySourceHash, M as yellow, Mt as formatDockerLoginError, N as IAMRoleProvider, Nn as normalizeAwsError, Nt as getDockerCmd, O as cyan, On as StackTerminationProtectionError, Ot as parseBootstrapMarker, P as collectInlinePolicyNamesManagedBySiblings, Pn as withErrorHandling, Pt as runDockerForeground, Q as configBooleanRefusal, Qt as CFN_TEMPLATE_URL_LIMIT, R as findSilentDropProperties, Rt as Synthesizer, S as extractDeploymentEventError, Sn as NestedStackChildDirectDestroyError, St as loadPublishableAssetManifest, T as renderStatefulReason, Tn as ResourceTimeoutError, Tt as BOOTSTRAP_MARKER_PREFIX, U as IntrinsicFunctionResolver, Ut as resolveAutoAssetStorage, V as disableInstanceApiTermination, Vt as getLegacyStateBucketName, W as cfnRefValueFromPhysicalId, Wt as resolveCaptureObservedState, X as assertRegionMatch, Xt as warnDeprecatedNoPrefixCliFlag, Y as resolveExplicitPhysicalId, Yt as stateBucketExistenceConfirmed, Z as coerceCfnBoolean, Zt as CFN_TEMPLATE_BODY_LIMIT, _ as createPreDeleteFinalSnapshot, _n as LocalInvokeBuildError$1, _t as AssetPublisher, a as DeploymentEventsStore, an as processStackMessages, at as applyRoleArnIfSet, b as unsupportedFinalSnapshotError, bt as buildAssetRedirectMap, c as replayFailedOperations, cn as AwsClients, ct as withRetry, d as IMPLICIT_DELETE_DEPENDENCIES, dn as setAwsClients, dt as DagBuilder, en as findLargeInlineResources, et as readConfigString, f as computeImplicitDeleteEdges, ft as TemplateParser, g as ccRoutedFinalSnapshotError, gn as DeployCancelledError, gt as shouldRetainResource, h as buildFinalSnapshotIdentifier, ht as rebuildClientForBucketRegion, i as DeploymentEventsReader, in as AssemblyReader, it as requireConfigString, j as red, jt as buildDockerImage, k as gray, kt as validateAssetBucketName, l as replayRollback, ln as getAwsClients, lt as isRetryableTransientError, m as PRE_DELETE_SNAPSHOT_TYPES, mt as S3StateBackend, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as expectedOwnerParam, nt as requireConfigArray, o as planFailedOps, ot as DiffCalculator, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as CdkdError, pt as LockManager, q as WAFv2WebACLProvider, qt as resolveStateBucketWithDefaultAndSource, r as DeployEngine, rn as derivePartitionAndUrlSuffix, rt as requireConfigObject, s as planRollback, sn as resolveBucketRegion, st as describeTypeWithThrottleRetry, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as uploadCfnTemplate, tt as replayWarn, u as withResourceDeadline, un as resetAwsClients, ut as isThrottlingError, v as isFinalSnapshotError, vn as LocalMigrateError, vt as stringifyValue, w as isStatefulRecreateTargetSync, wn as ProvisioningError, wt as AssetModeResolver, x as makeCanonicalizePropertiesFn, xn as MissingCdkCliError, xt as createAssetRedirectResolver, y as refusesFinalSnapshot, yn as LocalStartServiceError, yt as WorkGraph, z as CloudControlProvider, zt as synthesisStatusMessage } from "./deploy-engine-uxo_FCje.js";
2
+ import { $ as configStringRefusal, $t as MIGRATE_TMP_PREFIX, A as green, At as validateContainerRepoName, B as slowCcOperationTimeoutMs, Bt as getDefaultStateBucketName, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as PartialFailureError, Ct as rewriteTemplateAssetReferences, D as bold, Dn as StackHasActiveImportsError, Dt as getBootstrapMarkerKey, E as formatResourceLine, En as ResourceUpdateNotSupportedError, Et as ensureAssetStorage, F as clearOnUpdateRemoval, Ft as runDockerStreaming, G as getAccountInfo, Gt as resolveSkipPrefix, H as isTerminationProtectionPropagationError, Ht as resolveApp, I as ProviderRegistry, It as AssetManifestLoader, J as normalizeAwsTagsToCfn, Jt as resolveUseCdkBootstrapAssets, K as refStateLookupFromResource, Kt as resolveStateBucketWithDefault, L as findActionableSilentDrops, Lt as getDockerImageBySourceHash, M as yellow, Mt as formatDockerLoginError, N as IAMRoleProvider, Nn as normalizeAwsError, Nt as getDockerCmd, O as cyan, On as StackTerminationProtectionError, Ot as parseBootstrapMarker, P as collectInlinePolicyNamesManagedBySiblings, Pn as withErrorHandling, Pt as runDockerForeground, Q as configBooleanRefusal, Qt as CFN_TEMPLATE_URL_LIMIT, R as findSilentDropProperties, Rt as Synthesizer, S as extractDeploymentEventError, Sn as NestedStackChildDirectDestroyError, St as loadPublishableAssetManifest, T as renderStatefulReason, Tn as ResourceTimeoutError, Tt as BOOTSTRAP_MARKER_PREFIX, U as IntrinsicFunctionResolver, Ut as resolveAutoAssetStorage, V as disableInstanceApiTermination, Vt as getLegacyStateBucketName, W as cfnRefValueFromPhysicalId, Wt as resolveCaptureObservedState, X as assertRegionMatch, Xt as warnDeprecatedNoPrefixCliFlag, Y as resolveExplicitPhysicalId, Yt as stateBucketExistenceConfirmed, Z as coerceCfnBoolean, Zt as CFN_TEMPLATE_BODY_LIMIT, _ as createPreDeleteFinalSnapshot, _n as LocalInvokeBuildError$1, _t as AssetPublisher, a as DeploymentEventsStore, an as processStackMessages, at as applyRoleArnIfSet, b as unsupportedFinalSnapshotError, bt as buildAssetRedirectMap, c as replayFailedOperations, cn as AwsClients, ct as withRetry, d as IMPLICIT_DELETE_DEPENDENCIES, dn as setAwsClients, dt as DagBuilder, en as findLargeInlineResources, et as readConfigString, f as computeImplicitDeleteEdges, ft as TemplateParser, g as ccRoutedFinalSnapshotError, gn as DeployCancelledError, gt as shouldRetainResource, h as buildFinalSnapshotIdentifier, ht as rebuildClientForBucketRegion, i as DeploymentEventsReader, in as AssemblyReader, it as requireConfigString, j as red, jt as buildDockerImage, k as gray, kt as validateAssetBucketName, l as replayRollback, ln as getAwsClients, lt as isRetryableTransientError, m as PRE_DELETE_SNAPSHOT_TYPES, mt as S3StateBackend, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as expectedOwnerParam, nt as requireConfigArray, o as planFailedOps, ot as DiffCalculator, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as CdkdError, pt as LockManager, q as WAFv2WebACLProvider, qt as resolveStateBucketWithDefaultAndSource, r as DeployEngine, rn as derivePartitionAndUrlSuffix, rt as requireConfigObject, s as planRollback, sn as resolveBucketRegion, st as describeTypeWithThrottleRetry, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as uploadCfnTemplate, tt as replayWarn, u as withResourceDeadline, un as resetAwsClients, ut as isThrottlingError, v as isFinalSnapshotError, vn as LocalMigrateError, vt as stringifyValue, w as isStatefulRecreateTargetSync, wn as ProvisioningError, wt as AssetModeResolver, x as makeCanonicalizePropertiesFn, xn as MissingCdkCliError, xt as createAssetRedirectResolver, y as refusesFinalSnapshot, yn as LocalStartServiceError, yt as WorkGraph, z as CloudControlProvider, zt as synthesisStatusMessage } from "./deploy-engine-BkXNtI7L.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-CCSQkGwO.js";
4
+ import { t as ASGProvider } from "./asg-provider-CxK7d3pH.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";
@@ -58338,9 +58338,17 @@ function stackTreeToJson(roots) {
58338
58338
  * PR B2 — until that lands, the orchestrator hard-errors at submission
58339
58339
  * time with a clear pointer.
58340
58340
  *
58341
- * The list is intentionally narrow. Other resource types CFn may not yet
58342
- * support for import are surfaced as errors by the CreateChangeSet call
58343
- * itself; we do not try to maintain a closed allowlist here.
58341
+ * The list is intentionally narrow, and stays hand-written because each member
58342
+ * is a cdkd / CDK construct rather than something the registry describes.
58343
+ *
58344
+ * It is NOT the only refusal, though — the "defer to the CreateChangeSet
58345
+ * error" note this comment used to end on was revisited in issue
58346
+ * [#1659](https://github.com/go-to-k/cdkd/issues/1659). Types AWS refuses for
58347
+ * IMPORT because their registry schema declares no `read` handler are now
58348
+ * caught by {@link buildImportPlan}'s pre-flight instead, which needs no
58349
+ * hand-maintained list (the signal is derived from the schema cdkd already
58350
+ * fetches for the identifier) and names every offender in one pass — AWS's own
58351
+ * error names only some of them.
58344
58352
  */
58345
58353
  const NEVER_IMPORTABLE_TYPES = /* @__PURE__ */ new Set(["AWS::CDK::Metadata"]);
58346
58354
  /**
@@ -58513,6 +58521,16 @@ const COMPOSITE_ID_SPLITTERS = {
58513
58521
  propertiesOverlay: { ApiId: apiId }
58514
58522
  };
58515
58523
  },
58524
+ "AWS::S3Tables::Namespace": (physicalId) => {
58525
+ const parts = physicalId.split("|");
58526
+ if (parts.length !== 2) throw new Error(`expected 2 parts ('<tableBucketARN>|<namespaceName>'), got ${parts.length}: '${physicalId}'`);
58527
+ const [tableBucketARN, namespace] = parts;
58528
+ if (!tableBucketARN || !namespace) throw new Error(`empty part in '<tableBucketARN>|<namespaceName>': '${physicalId}'`);
58529
+ return { resourceIdentifier: {
58530
+ TableBucketARN: tableBucketARN,
58531
+ Namespace: namespace
58532
+ } };
58533
+ },
58516
58534
  "AWS::Lambda::Permission": (physicalId, properties) => {
58517
58535
  const functionName = readStringProperty(properties, "FunctionName", "AWS::Lambda::Permission");
58518
58536
  return {
@@ -58598,6 +58616,144 @@ function readStringProperty(properties, key, resourceType) {
58598
58616
  return v;
58599
58617
  }
58600
58618
  /**
58619
+ * Types whose cdkd physicalId is a COMPOSITE (`|`-joined) string while
58620
+ * CloudFormation's `primaryIdentifier` is a SINGLE field — so the physicalId is
58621
+ * NOT the identifier value, and it is not a SPLIT of the composite either
58622
+ * (issue [#1659](https://github.com/go-to-k/cdkd/issues/1659)).
58623
+ *
58624
+ * ## Why this table exists next to `COMPOSITE_ID_SPLITTERS`
58625
+ *
58626
+ * `resolveResourceIdentifier` used to conflate two independent questions: the
58627
+ * ARITY of the CFn identifier, and the FORMAT of cdkd's physical id. The
58628
+ * single-field branch assumed "the physicalId IS the identifier value", which
58629
+ * holds for every type whose id is a scalar and silently produces a wrong value
58630
+ * for a composite-id type whose CFn identifier happens to be single-field —
58631
+ * `{ TableARN: 'arn:aws:s3tables:...|analytics|events' }` for an
58632
+ * `AWS::S3Tables::Table`. `COMPOSITE_ID_SPLITTERS` cannot fix that: it is only
58633
+ * consulted on the multi-field branch, so registering a type there would add an
58634
+ * entry no code path reaches.
58635
+ *
58636
+ * This table is consulted BEFORE the arity branch, so the two concerns are
58637
+ * separated: a registered type never takes the "physicalId is the identifier"
58638
+ * assumption regardless of what the schema's field count says.
58639
+ *
58640
+ * ## Why a RESOLVE and not a split
58641
+ *
58642
+ * The correct identifier for each member is a different value entirely, not a
58643
+ * segment of the composite — an ARN AWS minted, or an `sgr-` rule id. So each
58644
+ * entry reads the value cdkd recorded in state (`attributes`), and refuses when
58645
+ * it is absent rather than guessing.
58646
+ *
58647
+ * ## Live measurement (us-east-1, 2026-08-13)
58648
+ *
58649
+ * Every field below is BOTH the type's single-field `primaryIdentifier` AND
58650
+ * `readOnlyProperties`, which is why each resolution returns an EMPTY
58651
+ * `propertiesOverlay`: writing a read-only property into the synth template's
58652
+ * `Properties` block is rejected by CFn at changeset-create.
58653
+ *
58654
+ * | Type | `primaryIdentifier` | recorded as |
58655
+ * |---|---|---|
58656
+ * | `AWS::AppSync::DataSource` | `DataSourceArn` | `attributes.DataSourceArn` |
58657
+ * | `AWS::AppSync::Resolver` | `ResolverArn` | `attributes.ResolverArn` |
58658
+ * | `AWS::S3Tables::Table` | `TableARN` | `attributes.TableARN` |
58659
+ * | `AWS::EC2::SecurityGroupIngress` | `Id` | NOTHING (issue #1761) |
58660
+ *
58661
+ * All four declare a `read` handler, so CFn genuinely does accept them for
58662
+ * IMPORT — unlike `AWS::Glue::Table`, the type issue #1659's title names, which
58663
+ * CFn rejects outright (see {@link typeIsImportUnsupported}).
58664
+ */
58665
+ const COMPOSITE_PHYSICAL_ID_IDENTIFIERS = {
58666
+ "AWS::AppSync::DataSource": recordedArnIdentifier({
58667
+ resourceType: "AWS::AppSync::DataSource",
58668
+ field: "DataSourceArn",
58669
+ physicalIdShape: "<apiId>|<name>",
58670
+ arnServiceSegment: "appsync"
58671
+ }),
58672
+ "AWS::AppSync::Resolver": recordedArnIdentifier({
58673
+ resourceType: "AWS::AppSync::Resolver",
58674
+ field: "ResolverArn",
58675
+ physicalIdShape: "<apiId>|<typeName>|<fieldName>",
58676
+ arnServiceSegment: "appsync"
58677
+ }),
58678
+ "AWS::S3Tables::Table": recordedArnIdentifier({
58679
+ resourceType: "AWS::S3Tables::Table",
58680
+ field: "TableARN",
58681
+ physicalIdShape: "<tableBucketARN>|<namespace>|<name>",
58682
+ arnServiceSegment: "s3tables"
58683
+ }),
58684
+ "AWS::EC2::SecurityGroupIngress": {
58685
+ field: "Id",
58686
+ resolve: ({ logicalId }) => {
58687
+ throw new Error(`cdkd's physical id for AWS::EC2::SecurityGroupIngress is the composite '<groupId>|<ipProtocol>|<fromPort>|<toPort>', but CloudFormation IMPORT identifies the rule by the 'sgr-...' security-group rule id AWS assigns, which cdkd does not record in state (tracked in https://github.com/go-to-k/cdkd/issues/1761). Remove '${logicalId}' from the stack before exporting (the rule stays in AWS and can be re-declared in CloudFormation afterwards), or destroy it first and let CloudFormation create it fresh.`);
58688
+ }
58689
+ }
58690
+ };
58691
+ /**
58692
+ * Build the {@link CompositePhysicalIdIdentifier} for a type whose CFn
58693
+ * identifier is an ARN cdkd records in `attributes` under the SAME name as the
58694
+ * `primaryIdentifier` field.
58695
+ *
58696
+ * Two accepted sources, mirroring the splitter family's "both shapes exist in
58697
+ * state in the wild" contract:
58698
+ *
58699
+ * 1. the recorded attribute, which is what a fresh deploy and a cdkd `import`
58700
+ * both write; and
58701
+ * 2. a physicalId that is ALREADY the bare ARN. Two live producers:
58702
+ * `S3TablesProvider.importTable` records it for a Cloud-Control-routed
58703
+ * table (CC's identifier for the type IS the ARN), and an AppSync child
58704
+ * adopted through `cdkd import --migrate-from-cloudformation` records
58705
+ * CloudFormation's `PhysicalResourceId` verbatim, which is likewise the
58706
+ * ARN.
58707
+ *
58708
+ * **Both sources are validated by the SAME predicate.** An earlier cut checked
58709
+ * the ARN's shape only on source 2 — the rare one — while source 1, which
58710
+ * fires on essentially every export, accepted any non-blank string verbatim.
58711
+ * That is the wrong way round: a hand-edited state row, a row written by an
58712
+ * older binary that recorded the composite under the attribute name, or an ARN
58713
+ * for a different service sitting under `attributes.<field>` would each have
58714
+ * been shipped as the IMPORT identifier and silently adopted the WRONG
58715
+ * resource, which is precisely what this table exists to prevent.
58716
+ *
58717
+ * Anything failing both sources is refused. That covers the degraded record
58718
+ * `docs/state-management.md` describes — written by a cdkd older than the fix
58719
+ * that started recording the ARN, or by an import that could not reach STS —
58720
+ * and the remedy is the one that doc already gives.
58721
+ */
58722
+ function recordedArnIdentifier(options) {
58723
+ const { resourceType, field, physicalIdShape, arnServiceSegment } = options;
58724
+ /**
58725
+ * Is this value usable AS the type's CFn identifier?
58726
+ *
58727
+ * - `arn:` prefix and the type's own SERVICE segment: an ARN naming another
58728
+ * service is a different value entirely. Partition-agnostic by
58729
+ * construction, so `arn:aws-cn:s3tables:...` matches.
58730
+ * - no `|`: cdkd's composite for `AWS::S3Tables::Table` is built FROM the
58731
+ * bucket ARN, so it satisfies both checks above — the separator test is
58732
+ * what distinguishes the composite from the identifier, on BOTH sources.
58733
+ */
58734
+ const isUsableIdentifier = (value) => {
58735
+ const trimmed = value.trim();
58736
+ return trimmed.startsWith("arn:") && trimmed.includes(`:${arnServiceSegment}:`) && !trimmed.includes("|");
58737
+ };
58738
+ return {
58739
+ field,
58740
+ resolve: ({ logicalId, physicalId, attributes }) => {
58741
+ const recorded = attributes[field];
58742
+ if (typeof recorded === "string" && isUsableIdentifier(recorded)) return recorded.trim();
58743
+ if (isUsableIdentifier(physicalId)) return physicalId.trim();
58744
+ const recordedNote = typeof recorded === "string" && recorded.trim() ? `attributes.${field} is recorded as '${recorded.trim()}', which is not a ${arnServiceSegment} ARN` : `attributes.${field} is missing or empty`;
58745
+ throw new Error(`cdkd's physical id for ${resourceType} is the composite '${physicalIdShape}', but CloudFormation IMPORT identifies the resource by its ${field}, which cdkd state does not record usably for '${logicalId}' (${recordedNote}). Re-deploy the stack once so cdkd records ${field}, then re-run cdkd export.`);
58746
+ }
58747
+ };
58748
+ }
58749
+ /**
58750
+ * Every type registered in {@link COMPOSITE_PHYSICAL_ID_IDENTIFIERS}. Exported
58751
+ * so a test can assert the table's SIZE, not just membership — a new entry
58752
+ * needs a measurement row in the table's doc comment, and a size-blind test
58753
+ * would let one land silently.
58754
+ */
58755
+ const COMPOSITE_PHYSICAL_ID_IDENTIFIER_TYPES = Object.keys(COMPOSITE_PHYSICAL_ID_IDENTIFIERS);
58756
+ /**
58601
58757
  * Resource types AWS does NOT support in IMPORT changesets but DOES
58602
58758
  * support normal CREATE for. cdkd handles these via a pre-delete + phase-2
58603
58759
  * CREATE dance (see {@link RecreateBeforePhase2Entry}). When the user
@@ -58751,17 +58907,20 @@ async function exportCommand(stackArg, options) {
58751
58907
  }
58752
58908
  }
58753
58909
  }
58910
+ const { phase1Imports, phase2Creates, recreateBeforePhase2, nestedStackRows, blocked } = await buildImportPlan(state, template, awsClients.cloudFormation, resolvedStackName, {
58911
+ recreateImportUnsupported: options.recreateImportUnsupported,
58912
+ skipImportSupportPreflight: options.skipImportSupportPreflight
58913
+ });
58914
+ if (blocked.length > 0) {
58915
+ logger.error("The following resources block migration:");
58916
+ for (const b of blocked) logger.error(` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
58917
+ throw new Error(`${blocked.length} resource(s) block migration. Either destroy them first (cdkd destroy / cdkd state destroy cherry-picked), or remove them from the CDK app and re-synthesize.`);
58918
+ }
58754
58919
  const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
58755
58920
  if (!options.dryRun) {
58756
58921
  if (!await lockManager.acquireLock(resolvedStackName, targetRegion, owner, "export")) throw new Error(`Could not acquire lock for stack '${resolvedStackName}' (${targetRegion}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${resolvedStackName}' if you are certain no other process is active.`);
58757
58922
  }
58758
58923
  try {
58759
- const { phase1Imports, phase2Creates, recreateBeforePhase2, nestedStackRows, blocked } = await buildImportPlan(state, template, awsClients.cloudFormation, resolvedStackName, { recreateImportUnsupported: options.recreateImportUnsupported });
58760
- if (blocked.length > 0) {
58761
- logger.error("The following resources block migration:");
58762
- for (const b of blocked) logger.error(` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
58763
- throw new Error(`${blocked.length} resource(s) block migration. Either destroy them first (cdkd destroy / cdkd state destroy cherry-picked), or remove them from the CDK app and re-synthesize.`);
58764
- }
58765
58924
  if (nestedStackRows.length > 0) {
58766
58925
  const nestedStackTree = await buildCdkdStateStackTree(resolvedStackName, targetRegion, stateBackend, state);
58767
58926
  reportDriftBaselineGaps(state, logger);
@@ -58804,7 +58963,8 @@ async function exportCommand(stackArg, options) {
58804
58963
  dryRun: options.dryRun,
58805
58964
  yes: options.yes,
58806
58965
  includeNonImportable: options.includeNonImportable,
58807
- recreateImportUnsupported: options.recreateImportUnsupported
58966
+ recreateImportUnsupported: options.recreateImportUnsupported,
58967
+ skipImportSupportPreflight: options.skipImportSupportPreflight
58808
58968
  }
58809
58969
  })).outcome === "success") printNextSteps({
58810
58970
  cfnStackName: options.cfnStackName ?? cdkd2cfnStackName(resolvedStackName),
@@ -59343,7 +59503,16 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
59343
59503
  }
59344
59504
  let resolved;
59345
59505
  try {
59346
- resolved = await resolveResourceIdentifier(resourceType, stateEntry.physicalId, stateEntry.properties ?? {}, cfnClient, identifierCache);
59506
+ const schemaInfo = await cachedTypeSchemaInfo(resourceType, cfnClient, identifierCache);
59507
+ if (!options.skipImportSupportPreflight && typeIsImportUnsupported(schemaInfo)) {
59508
+ blocked.push({
59509
+ logicalId,
59510
+ resourceType,
59511
+ reason: `AWS CloudFormation does not support ${resourceType} in IMPORT changesets: its CloudFormation registry schema declares no 'read' handler (and reports the type as NON_PROVISIONABLE), and IMPORT needs that handler to look the resource up by identifier. CreateChangeSet would reject the whole export with "ResourceTypes [${resourceType}] are not supported for Import". Remove the resource from the stack before exporting (it stays in AWS and can be re-declared in CloudFormation afterwards), or destroy it first and let CloudFormation create it fresh. If AWS has since added IMPORT support for this type, please open a cdkd issue.`
59512
+ });
59513
+ continue;
59514
+ }
59515
+ resolved = resolveResourceIdentifier(resourceType, logicalId, stateEntry.physicalId, stateEntry.properties ?? {}, stateEntry.attributes ?? {}, schemaInfo);
59347
59516
  } catch (err) {
59348
59517
  blocked.push({
59349
59518
  logicalId,
@@ -59373,6 +59542,11 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
59373
59542
  * `ResourcesToImport[].ResourceIdentifier` for the given resource type
59374
59543
  * and cdkd state's physical ID.
59375
59544
  *
59545
+ * Types whose cdkd physicalId is COMPOSITE while the CFn identifier is a
59546
+ * single field (`COMPOSITE_PHYSICAL_ID_IDENTIFIERS`): resolved from cdkd
59547
+ * state's recorded attributes, BEFORE the field-count branch below — the
59548
+ * physicalId is neither the identifier nor a source to split (issue #1659).
59549
+ *
59376
59550
  * Single-key types: the map has a single entry keyed by the schema's
59377
59551
  * primaryIdentifier field name (e.g. `{ BucketName: 'my-bucket' }`).
59378
59552
  *
@@ -59382,16 +59556,27 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
59382
59556
  * into one entry per field. Composite types without a registered
59383
59557
  * splitter surface a clear error pointing at where to add one.
59384
59558
  *
59385
- * Prefers `DescribeType` (the authoritative AWS-internal registry) and
59386
- * falls back to a hardcoded single-key table when DescribeType fails
59387
- * (insufficient permissions, throttling, obscure type without a registry
59388
- * entry).
59389
- */
59390
- async function resolveResourceIdentifier(resourceType, physicalId, properties, cfnClient, cache) {
59391
- let entry = cache.get(resourceType);
59392
- if (entry === void 0) {
59393
- entry = await fetchPrimaryIdentifier(resourceType, cfnClient);
59394
- cache.set(resourceType, entry);
59559
+ * `entry` is the caller's already-resolved registry-schema info — see
59560
+ * {@link cachedTypeSchemaInfo}, which prefers `DescribeType` (the authoritative
59561
+ * AWS-internal registry) and falls back to a hardcoded single-key table when
59562
+ * DescribeType fails (insufficient permissions, throttling, obscure type
59563
+ * without a registry entry). Taking it as a parameter rather than fetching is
59564
+ * what keeps `buildImportPlan` to ONE DescribeType per type across both the
59565
+ * IMPORT pre-flight and this resolution.
59566
+ */
59567
+ function resolveResourceIdentifier(resourceType, logicalId, physicalId, properties, attributes, entry) {
59568
+ const compositeIdentifier = COMPOSITE_PHYSICAL_ID_IDENTIFIERS[resourceType];
59569
+ if (compositeIdentifier) {
59570
+ if (entry.fields.length !== 1 || entry.fields[0] !== compositeIdentifier.field) throw new Error(`${resourceType} is registered in COMPOSITE_PHYSICAL_ID_IDENTIFIERS as identified by '${compositeIdentifier.field}', but CloudFormation now reports primaryIdentifier [${entry.fields.join(", ")}]. The registry schema changed under cdkd — update the entry in src/cli/commands/export.ts (or move the type to COMPOSITE_ID_SPLITTERS if its identifier is now composite too)`);
59571
+ const value = compositeIdentifier.resolve({
59572
+ logicalId,
59573
+ physicalId,
59574
+ attributes
59575
+ });
59576
+ return {
59577
+ resourceIdentifier: { [compositeIdentifier.field]: value },
59578
+ propertiesOverlay: {}
59579
+ };
59395
59580
  }
59396
59581
  if (entry.fields.length === 1) return { resourceIdentifier: { [entry.fields[0]]: physicalId } };
59397
59582
  const splitter = COMPOSITE_ID_SPLITTERS[resourceType];
@@ -59406,6 +59591,53 @@ async function resolveResourceIdentifier(resourceType, physicalId, properties, c
59406
59591
  return result;
59407
59592
  }
59408
59593
  /**
59594
+ * Cache-consulting wrapper around {@link fetchPrimaryIdentifier}. Shared by
59595
+ * `resolveResourceIdentifier` and `buildImportPlan`'s pre-flight so one
59596
+ * DescribeType per type answers both questions.
59597
+ */
59598
+ async function cachedTypeSchemaInfo(resourceType, cfnClient, cache) {
59599
+ const cached = cache.get(resourceType);
59600
+ if (cached !== void 0) return cached;
59601
+ const entry = await fetchPrimaryIdentifier(resourceType, cfnClient);
59602
+ cache.set(resourceType, entry);
59603
+ return entry;
59604
+ }
59605
+ /**
59606
+ * True when the registry schema says CloudFormation will refuse the type in an
59607
+ * IMPORT changeset (`ValidationError: ResourceTypes [<T>] are not supported
59608
+ * for Import`) — IMPORT looks the resource up by identifier through the type's
59609
+ * `read` handler, so a type without one cannot be adopted.
59610
+ *
59611
+ * Live-measured (us-east-1, 2026-08-13) against `CreateChangeSet
59612
+ * --change-set-type IMPORT`: `AWS::Glue::Table` / `AWS::Route53::RecordSet` /
59613
+ * `AWS::AppSync::ApiKey` / `AWS::EC2::NetworkAclEntry` /
59614
+ * `AWS::Route53::RecordSetGroup` / `AWS::SQS::QueuePolicy` /
59615
+ * `AWS::SNS::TopicPolicy` all lack a `read` handler, are all
59616
+ * `NON_PROVISIONABLE`, and were all rejected; `AWS::S3::Bucket`
59617
+ * (`FULLY_MUTABLE`, read handler present) passed the type check. Both shapes
59618
+ * of "no read handler" occur and both count: NO `handlers` block at all (the
59619
+ * legacy types) and a `handlers` block whose keys are only create / update /
59620
+ * delete (`AWS::EC2::NetworkAclEntry`).
59621
+ *
59622
+ * **Why the verdict needs TWO agreeing fields.** Reading "the `handlers` key is
59623
+ * missing" as a refusal on its own makes any partial or unusual response block
59624
+ * an export that works today — the one regression this pre-flight could cause,
59625
+ * and strictly worse than the status quo of letting AWS answer. So the refusal
59626
+ * also requires `ProvisioningType: NON_PROVISIONABLE`; a response satisfying
59627
+ * only one of the two resolves to `unknown` and is left to CreateChangeSet.
59628
+ *
59629
+ * The two were perfectly CORRELATED across the 21 types sampled on 2026-08-13
59630
+ * (every no-read-handler type was NON_PROVISIONABLE and vice versa), so on
59631
+ * today's registry the conjunction's real work is requiring a COMPLETE
59632
+ * DescribeType response before refusing. The `read` handler stays the primary
59633
+ * signal because it is the mechanistic reason CFn accepts a type for IMPORT —
59634
+ * refusing on `ProvisioningType` alone would reject a NON_PROVISIONABLE type
59635
+ * that does declare one, should AWS ever publish that combination.
59636
+ */
59637
+ function typeIsImportUnsupported(entry) {
59638
+ return entry.importSupport === "unsupported";
59639
+ }
59640
+ /**
59409
59641
  * Fetch the primary identifier field names for a resource type, with a
59410
59642
  * hardcoded single-key fallback when DescribeType is unavailable.
59411
59643
  */
@@ -59413,15 +59645,26 @@ async function fetchPrimaryIdentifier(resourceType, cfnClient) {
59413
59645
  try {
59414
59646
  const resp = await describeTypeWithThrottleRetry(resourceType, cfnClient);
59415
59647
  if (resp.Schema) {
59416
- const primary = JSON.parse(resp.Schema).primaryIdentifier;
59417
- if (Array.isArray(primary) && primary.length > 0 && primary.every((p) => typeof p === "string")) return { fields: primary.map((p) => p.replace(/^\/properties\//, "")) };
59648
+ const parsed = JSON.parse(resp.Schema);
59649
+ const primary = parsed.primaryIdentifier;
59650
+ if (Array.isArray(primary) && primary.length > 0 && primary.every((p) => typeof p === "string")) {
59651
+ const fields = primary.map((p) => p.replace(/^\/properties\//, ""));
59652
+ const handlers = parsed.handlers;
59653
+ return {
59654
+ fields,
59655
+ importSupport: typeof handlers === "object" && handlers !== null && Object.prototype.hasOwnProperty.call(handlers, "read") ? "importable" : resp.ProvisioningType === "NON_PROVISIONABLE" ? "unsupported" : "unknown"
59656
+ };
59657
+ }
59418
59658
  }
59419
59659
  } catch (err) {
59420
59660
  const msg = err instanceof Error ? err.message : String(err);
59421
59661
  getLogger().debug(`DescribeType failed for ${resourceType}: ${msg} — using fallback`);
59422
59662
  }
59423
59663
  const fallback = PRIMARY_IDENTIFIER_FALLBACK[resourceType];
59424
- if (fallback) return { fields: [fallback] };
59664
+ if (fallback) return {
59665
+ fields: [fallback],
59666
+ importSupport: "unknown"
59667
+ };
59425
59668
  throw new Error(`primary identifier unknown (DescribeType returned no usable schema and no fallback is registered). Add ${resourceType} to PRIMARY_IDENTIFIER_FALLBACK in export.ts, or open an issue.`);
59426
59669
  }
59427
59670
  /**
@@ -60347,7 +60590,10 @@ async function runPerStackImportLoop(args) {
60347
60590
  const perStackPlans = [];
60348
60591
  for (const node of leafFirst) {
60349
60592
  const meta = nodesByCdkdName.get(node.stackName);
60350
- const plan = await buildImportPlan(meta.state, meta.template, deps.cfnClient, meta.cdkdStackName, { recreateImportUnsupported: options.recreateImportUnsupported });
60593
+ const plan = await buildImportPlan(meta.state, meta.template, deps.cfnClient, meta.cdkdStackName, {
60594
+ recreateImportUnsupported: options.recreateImportUnsupported,
60595
+ skipImportSupportPreflight: options.skipImportSupportPreflight === true
60596
+ });
60351
60597
  if (plan.blocked.length > 0) {
60352
60598
  const lines = plan.blocked.map((b) => ` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
60353
60599
  throw new Error(`Stack '${meta.cdkdStackName}' has ${plan.blocked.length} resource(s) that block migration:\n${lines.join("\n")}`);
@@ -60763,7 +61009,7 @@ async function confirmPrompt$3(prompt) {
60763
61009
  }
60764
61010
  }
60765
61011
  function createExportCommand() {
60766
- const cmd = new Command("export").description("Hand a cdkd-managed stack over to CloudFormation via CFn IMPORT (changeset). AWS resources are unchanged; cdkd state for the stack is deleted on success. Mirror of `cdkd import` (AWS → cdkd) in the reverse direction (cdkd → CFn). Accepts JSON and YAML templates (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Aborts if any resource is not CFn-importable.").argument("[stack]", "Stack name to export (auto-detected for single-stack apps)").option("--cfn-stack-name <name>", "Name of the destination CloudFormation stack for the root stack. Defaults to the cdkd stack name (or the cdkd2cfnStackName mapping when the name contains '~'). For per-nested-child overrides, use --cfn-child-stack-name.").option("--cfn-child-stack-name <pair...>", "Per-nested-child CFn stack-name override. Repeatable. Format: '<cdkdName>=<cfnName>' (e.g. --cfn-child-stack-name 'MyApp~Database=my-app-db'). The cdkd stack name for a nested child is '<parent>~<childLogicalId>' (v6 state-key form). Without an override the CFn name is derived by replacing '~' with '-'. Only consulted when the exported stack tree contains nested children; ignored for flat stacks.").option("--template <path>", "Path to a pre-rendered CloudFormation template (JSON or YAML — format auto-detected). Skips synth.").option("--stack-region <region>", "Region of the cdkd state record to operate on. Required when the same stack name has state in multiple regions.").option("--dry-run", "Print the import plan without creating a changeset.", false).option("--accept-transient-context", "Allow CLI -c key=value overrides at export time even though they are not persisted to cdk.json / cdk.context.json (default: refuse). When set, the user is responsible for passing the same -c flags to every future cdk deploy.", false).option("--include-non-importable", "Run a 2-phase migration when the stack contains non-importable resources (Custom::*). Phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones, which re-invokes each Custom Resource's onCreate handler. Make sure onCreate is idempotent before enabling.", false).option("--parameter <key=value...>", "CFn template Parameter override, repeatable. Required when the synthesized template has Parameters without Default values; otherwise overrides the template's default value. Format: --parameter Key=Value.").option("--strict-cross-stack", "Refuse to export when sibling cdkd stacks in the same CDK app reference the exporting stack via Fn::GetStackOutput. Without the flag, cdkd warns and proceeds — the consumers keep resolving via the CloudFormation fallback (weak reference) unless they deploy with --no-cfn-fallback.", false).option("--no-recreate-import-unsupported", "Block instead of auto-handling resource types AWS does NOT support in IMPORT changesets (currently only AWS::ApiGatewayV2::Stage, emitted by CDK HttpApi). Default behavior: cdkd skips these from phase 1, deletes the AWS-side resource between phases, and lets CFn re-CREATE in phase 2 (brief unavailability window). With this flag, the export aborts with a clear error instead.").action(withErrorHandling(exportCommand));
61012
+ const cmd = new Command("export").description("Hand a cdkd-managed stack over to CloudFormation via CFn IMPORT (changeset). AWS resources are unchanged; cdkd state for the stack is deleted on success. Mirror of `cdkd import` (AWS → cdkd) in the reverse direction (cdkd → CFn). Accepts JSON and YAML templates (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Aborts if any resource is not CFn-importable.").argument("[stack]", "Stack name to export (auto-detected for single-stack apps)").option("--cfn-stack-name <name>", "Name of the destination CloudFormation stack for the root stack. Defaults to the cdkd stack name (or the cdkd2cfnStackName mapping when the name contains '~'). For per-nested-child overrides, use --cfn-child-stack-name.").option("--cfn-child-stack-name <pair...>", "Per-nested-child CFn stack-name override. Repeatable. Format: '<cdkdName>=<cfnName>' (e.g. --cfn-child-stack-name 'MyApp~Database=my-app-db'). The cdkd stack name for a nested child is '<parent>~<childLogicalId>' (v6 state-key form). Without an override the CFn name is derived by replacing '~' with '-'. Only consulted when the exported stack tree contains nested children; ignored for flat stacks.").option("--template <path>", "Path to a pre-rendered CloudFormation template (JSON or YAML — format auto-detected). Skips synth.").option("--stack-region <region>", "Region of the cdkd state record to operate on. Required when the same stack name has state in multiple regions.").option("--dry-run", "Print the import plan without creating a changeset.", false).option("--accept-transient-context", "Allow CLI -c key=value overrides at export time even though they are not persisted to cdk.json / cdk.context.json (default: refuse). When set, the user is responsible for passing the same -c flags to every future cdk deploy.", false).option("--include-non-importable", "Run a 2-phase migration when the stack contains non-importable resources (Custom::*). Phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones, which re-invokes each Custom Resource's onCreate handler. Make sure onCreate is idempotent before enabling.", false).option("--parameter <key=value...>", "CFn template Parameter override, repeatable. Required when the synthesized template has Parameters without Default values; otherwise overrides the template's default value. Format: --parameter Key=Value.").option("--strict-cross-stack", "Refuse to export when sibling cdkd stacks in the same CDK app reference the exporting stack via Fn::GetStackOutput. Without the flag, cdkd warns and proceeds — the consumers keep resolving via the CloudFormation fallback (weak reference) unless they deploy with --no-cfn-fallback.", false).option("--skip-import-support-preflight", "Skip the pre-flight that refuses resource types whose CloudFormation registry schema says CFn cannot IMPORT them (no read handler AND NON_PROVISIONABLE), and let CreateChangeSet answer instead. The pre-flight is a registry HEURISTIC, not AWS's supported-for-import list, so this is the escape hatch for a type AWS has since made importable. Expect the changeset to be rejected if the heuristic was right.", false).option("--no-recreate-import-unsupported", "Block instead of auto-handling resource types AWS does NOT support in IMPORT changesets (currently only AWS::ApiGatewayV2::Stage, emitted by CDK HttpApi). Default behavior: cdkd skips these from phase 1, deletes the AWS-side resource between phases, and lets CFn re-CREATE in phase 2 (brief unavailability window). With this flag, the export aborts with a clear error instead.").action(withErrorHandling(exportCommand));
60767
61013
  [
60768
61014
  ...commonOptions,
60769
61015
  ...appOptions,
@@ -74608,7 +74854,7 @@ function createMigrateCommand() {
74608
74854
  */
74609
74855
  function buildProgram() {
74610
74856
  const program = new Command();
74611
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.281.23");
74857
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.281.24");
74612
74858
  program.hook("preAction", (_thisCommand, actionCommand) => {
74613
74859
  const { profile } = actionCommand.optsWithGlobals();
74614
74860
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;