@go-to-k/cdkd 0.161.3 → 0.162.0

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 },
@@ -47040,6 +47325,7 @@ function discoverFunctionUrl(logicalId, resource, template, stackName) {
47040
47325
  apiVersion: "v2",
47041
47326
  stage: "$default",
47042
47327
  apiStackName: stackName,
47328
+ apiLogicalId: logicalId,
47043
47329
  ...lambdaCdkPath !== void 0 && { apiCdkPath: lambdaCdkPath },
47044
47330
  declaredAt: `${stackName}/${logicalId}`
47045
47331
  };
@@ -50800,17 +51086,32 @@ function isPlaceholder(segment) {
50800
51086
  //#endregion
50801
51087
  //#region src/local/cors-handler.ts
50802
51088
  /**
50803
- * Build a `apiLogicalId → CorsConfig | undefined` map. Walks the
50804
- * template once, picks every `AWS::ApiGatewayV2::Api`, and extracts its
50805
- * `Properties.CorsConfiguration` if any. APIs without CorsConfiguration
50806
- * (or whose CorsConfiguration is malformed) are NOT entered into the map.
51089
+ * Build a `logicalId → CorsConfig | undefined` map. Walks the template
51090
+ * once and picks two CORS-bearing resource types:
51091
+ *
51092
+ * - `AWS::ApiGatewayV2::Api` `Properties.CorsConfiguration`
51093
+ * (HTTP API v2; the original PR 8c surface)
51094
+ * - `AWS::Lambda::Url` → `Properties.Cors` (Function URL; issue #644)
51095
+ *
51096
+ * Both blocks are field-for-field identical in CFn schema (same
51097
+ * `AllowOrigins` / `AllowMethods` / `AllowHeaders` / `ExposeHeaders` /
51098
+ * `MaxAge` / `AllowCredentials`), so a single parser handles both. The
51099
+ * map key is the resource's own logical ID — that ID is later looked up
51100
+ * against `DiscoveredRoute.apiLogicalId` (set to the surface-bearing
51101
+ * resource at route-discovery time) so the preflight interceptor finds
51102
+ * the right config.
51103
+ *
51104
+ * Resources without a CORS block (or whose block is malformed) are NOT
51105
+ * entered into the map.
50807
51106
  */
50808
51107
  function buildCorsConfigByApiId(template) {
50809
51108
  const out = /* @__PURE__ */ new Map();
50810
51109
  const resources = template.Resources ?? {};
50811
51110
  for (const [logicalId, resource] of Object.entries(resources)) {
50812
- if (resource.Type !== "AWS::ApiGatewayV2::Api") continue;
50813
- const raw = (resource.Properties ?? {})["CorsConfiguration"];
51111
+ let raw;
51112
+ if (resource.Type === "AWS::ApiGatewayV2::Api") raw = (resource.Properties ?? {})["CorsConfiguration"];
51113
+ else if (resource.Type === "AWS::Lambda::Url") raw = (resource.Properties ?? {})["Cors"];
51114
+ else continue;
50814
51115
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
50815
51116
  const parsed = parseCorsConfiguration(raw);
50816
51117
  if (parsed) out.set(logicalId, parsed);
@@ -59764,7 +60065,7 @@ function reorderArgs(argv) {
59764
60065
  */
59765
60066
  async function main() {
59766
60067
  const program = new Command();
59767
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.161.3");
60068
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.162.0");
59768
60069
  program.addCommand(createBootstrapCommand());
59769
60070
  program.addCommand(createSynthCommand());
59770
60071
  program.addCommand(createListCommand());