@go-to-k/cdkd 0.161.4 → 0.162.1

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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { _ as withSkipPrefix, a as runDockerStreaming, c as getLogger, d as getLiveRenderer, f as PATTERN_B_NAME_PROPERTIES, g as generateResourceNameWithFallback, h as generateResourceName, i as runDockerForeground, n as formatDockerLoginError, p as PATTERN_B_RESOURCE_TYPES, r as getDockerCmd, u as runStackBuffered, v as withStackName } from "./docker-cmd-iDMcWcre.js";
3
- import { A as S3StateBackend, B as resolveCaptureObservedState, C as assertRegionMatch, D as DagBuilder, E as DiffCalculator, F as buildDockerImage, G as CFN_TEMPLATE_BODY_LIMIT, H as resolveStateBucketWithDefault, I as Synthesizer, J as findLargeInlineResources, K as CFN_TEMPLATE_URL_LIMIT, L as getDefaultStateBucketName, M as AssetPublisher, N as stringifyValue, O as TemplateParser, P as WorkGraph, Q as resolveBucketRegion, R as getLegacyStateBucketName, S as CloudControlProvider, T as applyRoleArnIfSet, U as resolveStateBucketWithDefaultAndSource, V as resolveSkipPrefix, W as warnDeprecatedNoPrefixCliFlag, X as AssemblyReader, Y as uploadCfnTemplate, _ as matchesCdkPath, a as withRetry, at as LocalStartServiceError, b as ProviderRegistry, bt as normalizeAwsError, c as bold, ct as NestedStackChildDirectDestroyError, d as green, dt as ResourceTimeoutError, et as CdkdError, f as red, ft as ResourceUpdateNotSupportedError, g as CDK_PATH_TAG, h as collectInlinePolicyNamesManagedBySiblings, ht as StackTerminationProtectionError, i as withResourceDeadline, it as LocalMigrateError, j as shouldRetainResource, k as LockManager, l as cyan, lt as PartialFailureError, m as IAMRoleProvider, mt as StackHasActiveImportsError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as IMPLICIT_DELETE_DEPENDENCIES, p as yellow, pt as RouteDiscoveryError, q as MIGRATE_TMP_PREFIX, r as DeployEngine, rt as LocalInvokeBuildError, s as formatResourceLine, st as MissingCdkCliError, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as gray, ut as ProvisioningError, v as normalizeAwsTagsToCfn, w as IntrinsicFunctionResolver, x as findActionableSilentDrops, xt as withErrorHandling, y as resolveExplicitPhysicalId, z as resolveApp } from "./deploy-engine-D4iGkZAC.js";
3
+ import { A as S3StateBackend, B as resolveCaptureObservedState, C as assertRegionMatch, D as DagBuilder, E as DiffCalculator, F as buildDockerImage, G as CFN_TEMPLATE_BODY_LIMIT, H as resolveStateBucketWithDefault, I as Synthesizer, J as findLargeInlineResources, K as CFN_TEMPLATE_URL_LIMIT, L as getDefaultStateBucketName, M as AssetPublisher, N as stringifyValue, O as TemplateParser, P as WorkGraph, Q as resolveBucketRegion, R as getLegacyStateBucketName, S as CloudControlProvider, T as applyRoleArnIfSet, U as resolveStateBucketWithDefaultAndSource, V as resolveSkipPrefix, W as warnDeprecatedNoPrefixCliFlag, X as AssemblyReader, Y as uploadCfnTemplate, _ as matchesCdkPath, a as withRetry, at as LocalStartServiceError, b as ProviderRegistry, bt as normalizeAwsError, c as bold, ct as NestedStackChildDirectDestroyError, d as green, dt as ResourceTimeoutError, et as CdkdError, f as red, ft as ResourceUpdateNotSupportedError, g as CDK_PATH_TAG, h as collectInlinePolicyNamesManagedBySiblings, ht as StackTerminationProtectionError, i as withResourceDeadline, it as LocalMigrateError, j as shouldRetainResource, k as LockManager, l as cyan, lt as PartialFailureError, m as IAMRoleProvider, mt as StackHasActiveImportsError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as IMPLICIT_DELETE_DEPENDENCIES, p as yellow, pt as RouteDiscoveryError, q as MIGRATE_TMP_PREFIX, r as DeployEngine, rt as LocalInvokeBuildError, s as formatResourceLine, st as MissingCdkCliError, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as gray, ut as ProvisioningError, v as normalizeAwsTagsToCfn, w as IntrinsicFunctionResolver, x as findActionableSilentDrops, xt as withErrorHandling, y as resolveExplicitPhysicalId, z as resolveApp } from "./deploy-engine-DEbogepd.js";
4
4
  import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-B15NAPbL.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -423,6 +423,46 @@ function parseAllowUnsupportedPropertiesToken(value, previous) {
423
423
  return [...previous ?? [], ...parsed];
424
424
  }
425
425
  const allowUnsupportedPropertiesOption = new Option("--allow-unsupported-properties <entries>", "Comma-separated <ResourceType>:<PropertyName> tokens to accept as silently dropped at deploy time. Escape hatch — the property will NOT be written to AWS, the deployed resource will be missing the field. Example: --allow-unsupported-properties AWS::Lambda::Function:LoggingConfig,AWS::RDS::DBInstance:CACertificateIdentifier").argParser(parseAllowUnsupportedPropertiesToken);
426
+ /**
427
+ * Issue [#615] — `--recreate-via-cc-api <LogicalId>` (repeatable). Each
428
+ * named resource is destroyed + recreated this deploy via Cloud Control
429
+ * API regardless of whether the existing state stamps it `sdk` or has
430
+ * no `provisionedBy` field (legacy). The new physical id stamps
431
+ * `provisionedBy: 'cc-api'` so all subsequent ops route via CC (sticky).
432
+ *
433
+ * Mirrors `--allow-unsupported-properties` per-resource explicit naming
434
+ * (one flag-instance per logical id), but the argument is a single
435
+ * logical id (no comma split — `MyLambda,Other` would be ambiguous
436
+ * since logical ids do not embed commas anyway).
437
+ *
438
+ * Format-checks each logical id against CFn's `Logical IDs are
439
+ * alphanumeric (A-Z, a-z, 0-9)` rule. A typo aborts at parse time so
440
+ * an unmatched id is surfaced immediately rather than silently
441
+ * skipped at deploy time.
442
+ */
443
+ const LOGICAL_ID_FORMAT = /^[A-Za-z][A-Za-z0-9]{0,254}$/;
444
+ function parseRecreateViaCcApiToken(value, previous) {
445
+ const token = value.trim();
446
+ if (!LOGICAL_ID_FORMAT.test(token)) throw new Error(`Invalid --recreate-via-cc-api value "${value}": expected a CloudFormation logical id (alphanumeric, starts with a letter, max 255 chars). One --recreate-via-cc-api flag per resource — repeat the flag for additional targets.`);
447
+ return [...previous ?? [], token];
448
+ }
449
+ const recreateViaCcApiOption = new Option("--recreate-via-cc-api <logicalId>", "Destroy + recreate the named resource (by CloudFormation logical id) via Cloud Control API in this deploy, so a top-level CFn property cdkd would otherwise silently drop reaches AWS via CC. Repeatable — pass the flag once per resource. Per-resource opt-in (no bulk / no per-stack shortcut) so the destroy-and-recreate cost is acknowledged for each target. Stateful resource types (RDS, DynamoDB, S3, EFS, ...) refuse unless --force-stateful-recreation is ALSO passed (two-flag protection). Cannot be combined with --allow-unsupported-properties on the same resource type and property.").argParser(parseRecreateViaCcApiToken);
450
+ /**
451
+ * Issue [#615] — `--force-stateful-recreation` (boolean) is the second
452
+ * flag required to allow `--recreate-via-cc-api` to operate on a
453
+ * stateful resource (RDS / DynamoDB / EFS / S3 with data / etc.). Two
454
+ * flags so the user explicitly opts into the data-loss footgun.
455
+ *
456
+ * The guard list of "stateful" types lives in
457
+ * `src/provisioning/stateful-types.ts` so it can be queried from both
458
+ * the CLI pre-flight and the deploy engine.
459
+ *
460
+ * There is no per-resource granularity on the force flag — when set,
461
+ * EVERY named recreate target bypasses the stateful guard. Per-resource
462
+ * force would create a false sense of granularity (the user is opting
463
+ * into a footgun; pretending to scope it per-resource is misleading).
464
+ */
465
+ const forceStatefulRecreationOption = new Option("--force-stateful-recreation", "Bypass the stateful-resource guard for --recreate-via-cc-api targets. Required when ANY named target is a stateful type (RDS / DynamoDB / EFS / S3 with data / Logs with retention / Cognito / Secrets / SSM / Glue / ECR / CloudFront / Kinesis / OpenSearch). Destroy + recreate loses ALL data in the resource — no automatic data migration. Triple-opt-in for CI use: --recreate-via-cc-api <id> --force-stateful-recreation --yes.").default(false);
426
466
  const deployOptions = [
427
467
  new Option("--concurrency <number>", "Maximum concurrent resource operations").default(10).argParser((value) => parseInt(value, 10)),
428
468
  new Option("--stack-concurrency <number>", "Maximum concurrent stack deployments").default(4).argParser((value) => parseInt(value, 10)),
@@ -438,6 +478,8 @@ const deployOptions = [
438
478
  new Option("-e, --exclusively", "Only deploy requested stacks, do not include dependencies").default(false),
439
479
  allowUnsupportedTypesOption,
440
480
  allowUnsupportedPropertiesOption,
481
+ recreateViaCcApiOption,
482
+ forceStatefulRecreationOption,
441
483
  ...resourceTimeoutOptions
442
484
  ];
443
485
  /**
@@ -883,6 +925,219 @@ function createListCommand() {
883
925
  return cmd;
884
926
  }
885
927
 
928
+ //#endregion
929
+ //#region src/provisioning/stateful-types.ts
930
+ /**
931
+ * Stateful-resource guard list (issue [#615]).
932
+ *
933
+ * `--recreate-via-cc-api <LogicalId>` destroys + recreates the named
934
+ * resource in one deploy so a previously-silent-dropped top-level CFn
935
+ * property reaches AWS via Cloud Control API. For most types this is
936
+ * safe — destroying + recreating an IAM Role or a Lambda Function
937
+ * loses no user data — but for **data-bearing** types the destroy
938
+ * cycle loses everything in the resource: rows in a DynamoDB table,
939
+ * objects in an S3 bucket, log lines in a LogGroup, images in an ECR
940
+ * repository, etc.
941
+ *
942
+ * To avoid an accidental data-loss footgun, cdkd refuses to recreate
943
+ * any resource whose type is in {@link STATEFUL_TYPES} unless the user
944
+ * ALSO passes `--force-stateful-recreation`. The two-flag protection
945
+ * mirrors `--remove-protection`'s pattern (see
946
+ * `src/cli/commands/destroy-runner.ts`).
947
+ *
948
+ * The list is hand-curated and intentionally **conservative**: every
949
+ * type here carries user data that the AWS service does NOT
950
+ * automatically migrate to the replacement resource. Types that the
951
+ * AWS service treats as ephemeral (e.g. Lambda Function, IAM Role)
952
+ * are NOT in this list — recreate is cheap.
953
+ *
954
+ * Two entries are **conditionally stateful** — they only count when
955
+ * the resource actually contains data:
956
+ *
957
+ * - `AWS::S3::Bucket`: empty buckets are safe to recreate. The
958
+ * deploy engine probes `s3:ListObjectsV2` at plan time and only
959
+ * refuses when the bucket has at least one object.
960
+ * - `AWS::Logs::LogGroup`: a log group with `RetentionInDays`
961
+ * undefined or zero is functionally ephemeral. The deploy engine
962
+ * refuses only when `RetentionInDays > 0`.
963
+ *
964
+ * Both conditional checks live in {@link isStatefulRecreateTarget};
965
+ * the bare {@link STATEFUL_TYPES} set is the type-only first-cut.
966
+ */
967
+ const STATEFUL_TYPES = new Set([
968
+ "AWS::RDS::DBInstance",
969
+ "AWS::RDS::DBCluster",
970
+ "AWS::DocDB::DBInstance",
971
+ "AWS::DocDB::DBCluster",
972
+ "AWS::Neptune::DBInstance",
973
+ "AWS::Neptune::DBCluster",
974
+ "AWS::DynamoDB::Table",
975
+ "AWS::DynamoDB::GlobalTable",
976
+ "AWS::EFS::FileSystem",
977
+ "AWS::S3::Bucket",
978
+ "AWS::ECR::Repository",
979
+ "AWS::Kinesis::Stream",
980
+ "AWS::Elasticsearch::Domain",
981
+ "AWS::OpenSearchService::Domain",
982
+ "AWS::Cognito::UserPool",
983
+ "AWS::SecretsManager::Secret",
984
+ "AWS::SSM::Parameter",
985
+ "AWS::Glue::Database",
986
+ "AWS::Glue::Table",
987
+ "AWS::Logs::LogGroup",
988
+ "AWS::CloudFront::Distribution"
989
+ ]);
990
+ /**
991
+ * Multi-region resource types — `--recreate-via-cc-api` refuses these
992
+ * outright in v1 regardless of `--force-stateful-recreation`. Design
993
+ * doc §8 calls these "out of scope": the destroy + recreate cycle
994
+ * across replica regions is more involved than a single-region
995
+ * destroy-and-create (replica regions, automated backups, eventual
996
+ * consistency across the replication mesh, etc.).
997
+ *
998
+ * Distinct from {@link STATEFUL_TYPES} — STATEFUL_TYPES gates on data
999
+ * loss (bypassable with `--force-stateful-recreation`); this set is
1000
+ * an out-of-scope refusal (no bypass).
1001
+ */
1002
+ const MULTI_REGION_RECREATE_BLOCKED_TYPES = new Set(["AWS::DynamoDB::GlobalTable"]);
1003
+ /**
1004
+ * Cheap, synchronous read of the resource's recorded properties only.
1005
+ * For `AWS::S3::Bucket` this returns `null` — the live `ListObjectsV2`
1006
+ * probe to distinguish empty buckets (safe to recreate) from
1007
+ * non-empty (data loss) needs an S3 client + an AWS round-trip and is
1008
+ * deferred to a follow-up issue (v1 sync-defers; an `--force-stateful-
1009
+ * recreation` is recommended for any potentially-non-empty S3 target).
1010
+ *
1011
+ * Returns the {@link StatefulReason} when the type is stateful (or
1012
+ * `null` for non-stateful types).
1013
+ */
1014
+ function isStatefulRecreateTargetSync(resourceType, recordedProperties) {
1015
+ if (!STATEFUL_TYPES.has(resourceType)) return null;
1016
+ if (resourceType === "AWS::Logs::LogGroup") {
1017
+ const retention = recordedProperties?.["RetentionInDays"];
1018
+ if (typeof retention === "number" && retention > 0) return "has-retention";
1019
+ return null;
1020
+ }
1021
+ if (resourceType === "AWS::S3::Bucket") return null;
1022
+ return "always";
1023
+ }
1024
+ /**
1025
+ * Human-readable rendering of {@link StatefulReason} for error
1026
+ * messages. Used by the pre-flight guard's "X resources require
1027
+ * --force-stateful-recreation" listing.
1028
+ */
1029
+ function renderStatefulReason(reason) {
1030
+ switch (reason) {
1031
+ case "always": return "destroy loses all data in the resource";
1032
+ case "has-objects": return "S3 bucket is non-empty";
1033
+ case "has-retention": return "log group retains data (RetentionInDays > 0)";
1034
+ case null: return "(not stateful)";
1035
+ }
1036
+ }
1037
+
1038
+ //#endregion
1039
+ //#region src/deployment/recreate-targets.ts
1040
+ /**
1041
+ * Plan-time validation of the user's recreate-via-cc-api list.
1042
+ *
1043
+ * Pure with respect to AWS — does NOT probe S3 bucket emptiness. The
1044
+ * S3 conditional check is deferred to a follow-up issue (the live
1045
+ * `s3:ListObjectsV2` probe is out of scope for v1).
1046
+ *
1047
+ * Input order is preserved; duplicate logical ids in the user's input
1048
+ * are deduplicated.
1049
+ */
1050
+ const EMPTY_ALLOW_SET$1 = /* @__PURE__ */ new Set();
1051
+ function validateRecreateTargets(input) {
1052
+ const seen = /* @__PURE__ */ new Set();
1053
+ const targets = [];
1054
+ const unknownLogicalIds = [];
1055
+ const missingFromState = [];
1056
+ const ambiguousIntent = [];
1057
+ const blockedStatefulTargets = [];
1058
+ const blockedMultiRegionTargets = [];
1059
+ for (const logicalId of input.recreateViaCcApi) {
1060
+ if (seen.has(logicalId)) continue;
1061
+ seen.add(logicalId);
1062
+ const templateResource = input.template.Resources?.[logicalId];
1063
+ if (!templateResource) {
1064
+ unknownLogicalIds.push(logicalId);
1065
+ continue;
1066
+ }
1067
+ const recordedResource = input.state.resources[logicalId];
1068
+ if (!recordedResource) {
1069
+ missingFromState.push(logicalId);
1070
+ continue;
1071
+ }
1072
+ const resourceType = recordedResource.resourceType;
1073
+ const target = {
1074
+ logicalId,
1075
+ resourceType,
1076
+ physicalId: recordedResource.physicalId,
1077
+ statefulReason: isStatefulRecreateTargetSync(resourceType, recordedResource.properties)
1078
+ };
1079
+ targets.push(target);
1080
+ if (MULTI_REGION_RECREATE_BLOCKED_TYPES.has(resourceType)) blockedMultiRegionTargets.push(target);
1081
+ const actionableDrops = findActionableSilentDrops(resourceType, templateResource.Properties, EMPTY_ALLOW_SET$1);
1082
+ for (const { property } of actionableDrops) {
1083
+ const allowKey = `${resourceType}:${property}`;
1084
+ if (input.allowUnsupportedProperties.has(allowKey)) ambiguousIntent.push({
1085
+ logicalId,
1086
+ resourceType,
1087
+ property
1088
+ });
1089
+ }
1090
+ if (target.statefulReason !== null && !input.forceStatefulRecreation) blockedStatefulTargets.push(target);
1091
+ }
1092
+ return {
1093
+ targets,
1094
+ unknownLogicalIds,
1095
+ missingFromState,
1096
+ ambiguousIntent,
1097
+ blockedStatefulTargets,
1098
+ blockedMultiRegionTargets
1099
+ };
1100
+ }
1101
+ /**
1102
+ * Render the validation failures into a single multi-line error
1103
+ * message. Returns `null` when the validation was clean (no errors).
1104
+ * The deploy command throws this string as the message of a
1105
+ * `ProvisioningError` so the surface is `cdkd deploy` exit code 1
1106
+ * with the same shape as other pre-flight failures.
1107
+ */
1108
+ function renderRecreateTargetsErrors(validation) {
1109
+ const lines = [];
1110
+ if (validation.unknownLogicalIds.length > 0) {
1111
+ lines.push(`--recreate-via-cc-api named ${validation.unknownLogicalIds.length} logical id(s) not present in the synth template:`);
1112
+ for (const id of validation.unknownLogicalIds) lines.push(` - ${id}`);
1113
+ lines.push(" Fix: confirm each id exists in the template (CDK display path is the parent; the logical id is the CFn-emitted name, e.g. cdkd synth | jq '.Resources | keys'). Recreate operates on the synth template's logical ids, not CDK display paths.");
1114
+ }
1115
+ if (validation.missingFromState.length > 0) {
1116
+ if (lines.length > 0) lines.push("");
1117
+ lines.push(`--recreate-via-cc-api named ${validation.missingFromState.length} logical id(s) the template declares but cdkd state has no record of:`);
1118
+ for (const id of validation.missingFromState) lines.push(` - ${id}`);
1119
+ lines.push(" These are fresh CREATEs on the next deploy — recreate has nothing to destroy first. Remove the --recreate-via-cc-api flag for these resources; the new auto-route via Cloud Control (#614) handles fresh deploys.");
1120
+ }
1121
+ if (validation.ambiguousIntent.length > 0) {
1122
+ if (lines.length > 0) lines.push("");
1123
+ lines.push(`Ambiguous intent — ${validation.ambiguousIntent.length} resource(s) are named in BOTH --recreate-via-cc-api and --allow-unsupported-properties with the same Type:Prop on a silent-drop property the template uses:`);
1124
+ for (const overlap of validation.ambiguousIntent) lines.push(` - ${overlap.logicalId} (${overlap.resourceType}) — both --recreate-via-cc-api ${overlap.logicalId} (would migrate to CC, honoring ${overlap.property}) AND --allow-unsupported-properties ${overlap.resourceType}:${overlap.property} (would keep on SDK, accepting silent drop)`);
1125
+ lines.push(` Fix: pick ONE strategy per resource.`);
1126
+ }
1127
+ if (validation.blockedStatefulTargets.length > 0) {
1128
+ if (lines.length > 0) lines.push("");
1129
+ lines.push(`--recreate-via-cc-api would destroy + recreate ${validation.blockedStatefulTargets.length} stateful resource(s). Recreate loses ALL data — no automatic data migration. Re-run with --force-stateful-recreation to acknowledge the data-loss footgun.`);
1130
+ for (const blocked of validation.blockedStatefulTargets) lines.push(` - ${blocked.logicalId} (${blocked.resourceType}) — ${renderStatefulReason(blocked.statefulReason)}`);
1131
+ }
1132
+ if (validation.blockedMultiRegionTargets.length > 0) {
1133
+ if (lines.length > 0) lines.push("");
1134
+ lines.push(`--recreate-via-cc-api refuses to operate on ${validation.blockedMultiRegionTargets.length} multi-region resource(s) — out of scope for v1 of this flag (the destroy + recreate cycle across replica regions is more involved than the single-region path):`);
1135
+ for (const blocked of validation.blockedMultiRegionTargets) lines.push(` - ${blocked.logicalId} (${blocked.resourceType})`);
1136
+ lines.push(" No --force-stateful-recreation bypass — this category is structurally unsupported in v1. File an issue if you need this path.");
1137
+ }
1138
+ return lines.length > 0 ? lines.join("\n") : null;
1139
+ }
1140
+
886
1141
  //#endregion
887
1142
  //#region src/state/export-index-store.ts
888
1143
  /**
@@ -33339,10 +33594,40 @@ async function deployCommand(stacks, options) {
33339
33594
  if (!await promptMigrationConfirm(pending, { yes: options.yes })) return;
33340
33595
  }
33341
33596
  }
33597
+ let recreateViaCcApiTargets;
33598
+ if (options.recreateViaCcApi?.length) {
33599
+ const stateForRecreateCheck = await stackStateBackend.getState(stackInfo.stackName, stackRegion);
33600
+ const validation = validateRecreateTargets({
33601
+ template: stackInfo.template,
33602
+ state: stateForRecreateCheck?.state ?? {
33603
+ version: 7,
33604
+ stackName: stackInfo.stackName,
33605
+ region: stackRegion,
33606
+ resources: {},
33607
+ outputs: {},
33608
+ lastModified: Date.now()
33609
+ },
33610
+ recreateViaCcApi: options.recreateViaCcApi,
33611
+ allowUnsupportedProperties: new Set(options.allowUnsupportedProperties ?? []),
33612
+ forceStatefulRecreation: options.forceStatefulRecreation ?? false
33613
+ });
33614
+ const errorBlock = renderRecreateTargetsErrors(validation);
33615
+ if (errorBlock) throw new CdkdError(errorBlock, "RECREATE_VIA_CC_API_INVALID");
33616
+ recreateViaCcApiTargets = new Set(validation.targets.map((t) => t.logicalId));
33617
+ if (recreateViaCcApiTargets.size > 0) {
33618
+ logger.warn(`--recreate-via-cc-api will destroy + recreate ${recreateViaCcApiTargets.size} resource(s) via Cloud Control API on stack ${stackInfo.stackName}:`);
33619
+ for (const t of validation.targets) {
33620
+ const stateNote = t.statefulReason !== null ? ` ⚠ stateful (${t.statefulReason}) — --force-stateful-recreation acknowledged` : "";
33621
+ logger.warn(` - ${t.logicalId} (${t.resourceType})${stateNote}`);
33622
+ }
33623
+ logger.warn(" The destroy + recreate cycle is per-resource; sibling resources are unaffected. Downstream consumers of any recreated resource's outputs (Fn::GetStackOutput / Fn::ImportValue) will need a re-deploy to see the new physical id.");
33624
+ }
33625
+ }
33342
33626
  const deployEngineOptions = {
33343
33627
  concurrency: options.concurrency,
33344
33628
  dryRun: options.dryRun,
33345
33629
  noRollback: !options.rollback,
33630
+ ...recreateViaCcApiTargets && recreateViaCcApiTargets.size > 0 && { recreateViaCcApiTargets },
33346
33631
  captureObservedState: resolveCaptureObservedState(options.captureObservedState),
33347
33632
  ...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
33348
33633
  ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
@@ -50868,6 +51153,140 @@ function pickStringArray(value) {
50868
51153
  return out;
50869
51154
  }
50870
51155
  /**
51156
+ * Build a `fnUrlLogicalId → CorsConfig` map by tracing CloudFront →
51157
+ * Function URL chains in the template (issue #646).
51158
+ *
51159
+ * Production-correct CDK pattern: Function URL fronted by a CloudFront
51160
+ * Distribution where CORS is declared on the CloudFront
51161
+ * `ResponseHeadersPolicy` (NOT on the Function URL itself). Without this
51162
+ * helper, `cdkd local start-api` sees `Cors: null` on the Function URL
51163
+ * and emits no preflight headers — even though the CDK code correctly
51164
+ * declares the allowed origins on the CloudFront side.
51165
+ *
51166
+ * Detection: an `AWS::CloudFront::Distribution` whose `Origins[].DomainName`
51167
+ * matches the canonical CDK 2.x shape
51168
+ * `Fn::Select[2, Fn::Split['/', Fn::GetAtt[<FnUrlLogicalId>, 'FunctionUrl']]]`
51169
+ * is the chain marker. For each such origin, we walk every cache behavior
51170
+ * (`DefaultCacheBehavior` + `CacheBehaviors[]`), resolve their
51171
+ * `ResponseHeadersPolicyId: { Ref: <RhpLogicalId> }` to the
51172
+ * `AWS::CloudFront::ResponseHeadersPolicy` resource, and extract its
51173
+ * `Properties.ResponseHeadersPolicyConfig.CorsConfig`.
51174
+ *
51175
+ * Schema mapping (CloudFront → internal `CorsConfig`):
51176
+ *
51177
+ * AccessControlAllowOrigins.Items → AllowOrigins
51178
+ * AccessControlAllowMethods.Items → AllowMethods
51179
+ * AccessControlAllowHeaders.Items → AllowHeaders
51180
+ * AccessControlExposeHeaders.Items → ExposeHeaders
51181
+ * AccessControlMaxAgeSec → MaxAge
51182
+ * AccessControlAllowCredentials → AllowCredentials
51183
+ * (OriginOverride is ignored — cdkd has only one config slot)
51184
+ *
51185
+ * Multiple distributions fronting the same Function URL: last write
51186
+ * wins (rare in practice). Per-path CORS via `CacheBehaviors[]` is
51187
+ * NOT supported in v1 — the `DefaultCacheBehavior`'s policy applies
51188
+ * to all paths.
51189
+ */
51190
+ function buildCorsConfigFromCloudFrontChain(template) {
51191
+ const out = /* @__PURE__ */ new Map();
51192
+ const resources = template.Resources ?? {};
51193
+ for (const [, resource] of Object.entries(resources)) {
51194
+ if (resource.Type !== "AWS::CloudFront::Distribution") continue;
51195
+ const distConfig = (resource.Properties ?? {})["DistributionConfig"];
51196
+ if (!distConfig || typeof distConfig !== "object") continue;
51197
+ const dc = distConfig;
51198
+ const origins = Array.isArray(dc["Origins"]) ? dc["Origins"] : [];
51199
+ for (const origin of origins) {
51200
+ if (!origin || typeof origin !== "object") continue;
51201
+ const fnUrlLogicalId = pickFnUrlLogicalIdFromOriginDomainName(origin["DomainName"]);
51202
+ if (!fnUrlLogicalId) continue;
51203
+ const cacheBehaviors = [dc["DefaultCacheBehavior"], ...Array.isArray(dc["CacheBehaviors"]) ? dc["CacheBehaviors"] : []];
51204
+ for (const behavior of cacheBehaviors) {
51205
+ if (!behavior || typeof behavior !== "object") continue;
51206
+ const rhpId = pickRhpRefLogicalId(behavior["ResponseHeadersPolicyId"]);
51207
+ if (!rhpId) continue;
51208
+ const rhpResource = resources[rhpId];
51209
+ if (!rhpResource || rhpResource.Type !== "AWS::CloudFront::ResponseHeadersPolicy") continue;
51210
+ const rhpConfig = (rhpResource.Properties ?? {})["ResponseHeadersPolicyConfig"];
51211
+ if (!rhpConfig || typeof rhpConfig !== "object") continue;
51212
+ const corsConfig = rhpConfig["CorsConfig"];
51213
+ if (!corsConfig || typeof corsConfig !== "object" || Array.isArray(corsConfig)) continue;
51214
+ const parsed = parseCloudFrontCorsConfig(corsConfig);
51215
+ if (parsed) out.set(fnUrlLogicalId, parsed);
51216
+ }
51217
+ }
51218
+ }
51219
+ return out;
51220
+ }
51221
+ /**
51222
+ * Detect the canonical CDK 2.x `DomainName` shape that points a
51223
+ * CloudFront Origin at a Function URL:
51224
+ * {Fn::Select: [2, {Fn::Split: ['/', {Fn::GetAtt: [<id>, 'FunctionUrl']}]}]}
51225
+ * Returns the Function URL's logical ID, or undefined if the shape
51226
+ * doesn't match.
51227
+ */
51228
+ function pickFnUrlLogicalIdFromOriginDomainName(value) {
51229
+ if (!value || typeof value !== "object") return void 0;
51230
+ const sel = value["Fn::Select"];
51231
+ if (!Array.isArray(sel) || sel.length !== 2 || sel[0] !== 2) return void 0;
51232
+ const split = sel[1];
51233
+ if (!split || typeof split !== "object") return void 0;
51234
+ const splitArgs = split["Fn::Split"];
51235
+ if (!Array.isArray(splitArgs) || splitArgs.length !== 2 || splitArgs[0] !== "/") return void 0;
51236
+ const getAtt = splitArgs[1];
51237
+ if (!getAtt || typeof getAtt !== "object") return void 0;
51238
+ const ga = getAtt["Fn::GetAtt"];
51239
+ if (!Array.isArray(ga) || ga.length !== 2 || typeof ga[0] !== "string" || ga[1] !== "FunctionUrl") return;
51240
+ return ga[0];
51241
+ }
51242
+ /**
51243
+ * Unwrap a `ResponseHeadersPolicyId` value to its referenced logical
51244
+ * ID. CDK 2.x synthesizes this as `{ Ref: <id> }`. Returns undefined
51245
+ * for the AWS-managed-policy ID form (literal UUID string) since
51246
+ * cdkd can't fetch those — and for any non-Ref shape.
51247
+ */
51248
+ function pickRhpRefLogicalId(value) {
51249
+ if (!value || typeof value !== "object") return void 0;
51250
+ const ref = value["Ref"];
51251
+ if (typeof ref !== "string" || ref.length === 0) return void 0;
51252
+ return ref;
51253
+ }
51254
+ /**
51255
+ * Parse a CloudFront `ResponseHeadersPolicyConfig.CorsConfig` block
51256
+ * into the internal `CorsConfig` shape. Schema differs from Function
51257
+ * URL / HTTP API v2 (`AccessControl*` prefix + nested `Items` wrapper);
51258
+ * see `buildCorsConfigFromCloudFrontChain` JSDoc for the field mapping.
51259
+ *
51260
+ * Returns undefined when every value-bearing field is missing.
51261
+ */
51262
+ function parseCloudFrontCorsConfig(raw) {
51263
+ const allowOrigins = pickItemsStringArray(raw["AccessControlAllowOrigins"]);
51264
+ const allowMethods = pickItemsStringArray(raw["AccessControlAllowMethods"]);
51265
+ const allowHeaders = pickItemsStringArray(raw["AccessControlAllowHeaders"]);
51266
+ const exposeHeaders = pickItemsStringArray(raw["AccessControlExposeHeaders"]);
51267
+ const maxAgeRaw = raw["AccessControlMaxAgeSec"];
51268
+ const allowCreds = raw["AccessControlAllowCredentials"];
51269
+ if (allowOrigins.length === 0 && allowMethods.length === 0 && allowHeaders.length === 0 && exposeHeaders.length === 0 && maxAgeRaw === void 0 && allowCreds === void 0) return;
51270
+ const config = {
51271
+ AllowOrigins: allowOrigins,
51272
+ AllowMethods: allowMethods,
51273
+ AllowHeaders: allowHeaders,
51274
+ ExposeHeaders: exposeHeaders
51275
+ };
51276
+ if (typeof maxAgeRaw === "number" && Number.isFinite(maxAgeRaw)) config.MaxAge = Math.trunc(maxAgeRaw);
51277
+ if (typeof allowCreds === "boolean") config.AllowCredentials = allowCreds;
51278
+ return config;
51279
+ }
51280
+ /**
51281
+ * CloudFront `AccessControl*Origins/Methods/Headers` use a nested
51282
+ * `Items: string[]` wrapper. Unwrap to a plain `string[]`.
51283
+ */
51284
+ function pickItemsStringArray(value) {
51285
+ if (!value || typeof value !== "object") return [];
51286
+ const items = value["Items"];
51287
+ return pickStringArray(items);
51288
+ }
51289
+ /**
50871
51290
  * Try to match an OPTIONS preflight request against the given CORS
50872
51291
  * config. Returns the canonical response when every check passes;
50873
51292
  * `null` when the request didn't satisfy AllowOrigins / AllowMethods /
@@ -54243,8 +54662,10 @@ async function localStartApiCommand(target, options) {
54243
54662
  }
54244
54663
  const corsConfigByApiId = /* @__PURE__ */ new Map();
54245
54664
  for (const stack of targetStacks) {
54246
- const m = buildCorsConfigByApiId(stack.template);
54247
- for (const [k, v] of m) corsConfigByApiId.set(k, v);
54665
+ const fromCloudFront = buildCorsConfigFromCloudFrontChain(stack.template);
54666
+ for (const [k, v] of fromCloudFront) corsConfigByApiId.set(k, v);
54667
+ const direct = buildCorsConfigByApiId(stack.template);
54668
+ for (const [k, v] of direct) corsConfigByApiId.set(k, v);
54248
54669
  }
54249
54670
  const stateByStack = options.fromState || isCfnFlagPresent(options) ? await loadStateForRoutedStacks(targetStacks, routes, routesWithAuth, options) : /* @__PURE__ */ new Map();
54250
54671
  const lambdaIds = uniqueLambdaIds(routes, routesWithAuth, webSocketApis);
@@ -59780,7 +60201,7 @@ function reorderArgs(argv) {
59780
60201
  */
59781
60202
  async function main() {
59782
60203
  const program = new Command();
59783
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.161.4");
60204
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.162.1");
59784
60205
  program.addCommand(createBootstrapCommand());
59785
60206
  program.addCommand(createSynthCommand());
59786
60207
  program.addCommand(createListCommand());