@go-to-k/cdkd 0.283.13 → 0.283.15

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 coerceCfnBoolean, $t as resolveStateBucketWithDefault, A as cyan, An as MissingCdkCliError, At as AssetModeResolver, B as findSilentDropProperties, Bt as getDockerCmd, C as makeCanonicalizePropertiesFn, Ct as stringifyValue, D as renderStatefulReason, Dn as LocalMigrateError, Dt as loadPublishableAssetManifest, E as isStatefulRecreateTargetSync, En as LocalInvokeBuildError$1, Et as createAssetRedirectResolver, F as IAMRoleProvider, Fn as ResourceUpdateNotSupportedError, Ft as validateAssetBucketName, G as IntrinsicFunctionResolver, Gn as isRetryableTransientError, Gt as Synthesizer, H as slowCcOperationTimeoutMs, Hn as normalizeAwsError, Ht as runDockerStreaming, I as collectInlinePolicyNamesManagedBySiblings, In as StackHasActiveImportsError, It as validateContainerRepoName, J as refStateLookupFromResource, Jt as getLegacyStateBucketName, K as cfnRefValueFromPhysicalId, Kn as isThrottlingError, Kt as synthesisStatusMessage, L as clearOnUpdateRemoval, Ln as StackTerminationProtectionError, Lt as buildDenyExternalAccessPolicy, M as green, Mn as PartialFailureError, Mt as ensureAssetStorage, N as red, Nn as ProvisioningError, Nt as getBootstrapMarkerKey, O as formatResourceLine, On as LocalStartServiceError, Ot as rewriteTemplateAssetReferences, P as yellow, Pn as ResourceTimeoutError, Pt as parseBootstrapMarker, Q as assertRegionMatch, Qt as resolveSkipPrefix, R as ProviderRegistry, Rt as buildDockerImage, S as unsupportedFinalSnapshotError, Sn as CdkdError, St as AssetPublisher, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as DeployCancelledError, Tt as buildAssetRedirectMap, U as disableInstanceApiTermination, Un as withErrorHandling, Ut as AssetManifestLoader, V as CloudControlProvider, Vt as runDockerForeground, W as isTerminationProtectionPropagationError, Wn as isMarkedNonRetryable, Wt as getDockerImageBySourceHash, X as normalizeAwsTagsToCfn, Xt as resolveAutoAssetStorage, Y as WAFv2WebACLProvider, Yt as resolveApp, Z as resolveExplicitPhysicalId, Zt as resolveCaptureObservedState, _ as buildFinalSnapshotIdentifier, _n as AwsClients, _t as TemplateParser, a as DeploymentEventsStore, an as CFN_TEMPLATE_URL_LIMIT, at as requireConfigObject, b as isFinalSnapshotError, bn as setAwsClients, bt as rebuildClientForBucketRegion, c as replayFailedOperations, cn as uploadCfnTemplate, ct as s3BucketDomainName, d as deleteSkipReason, dn as canonicalizeRegion, dt as s3BucketWebsiteUrl, en as resolveStateBucketWithDefaultAndSource, et as configBooleanRefusal, f as withResourceDeadline, fn as derivePartitionAndUrlSuffix, ft as applyRoleArnIfSet, g as PRE_DELETE_SNAPSHOT_TYPES, gn as resolveBucketRegion, gt as DagBuilder, h as ATOMIC_FINAL_SNAPSHOT_TYPES, ht as withRetry, i as DeploymentEventsReader, in as CFN_TEMPLATE_BODY_LIMIT, it as requireConfigArray, j as gray, jn as NestedStackChildDirectDestroyError, jt as BOOTSTRAP_MARKER_PREFIX, k as bold, kt as escapeRegExp, l as replayRollback, ln as expectedOwnerParam, lt as s3BucketDualStackDomainName, m as computeImplicitDeleteEdges, mn as processStackMessages, mt as describeTypeWithThrottleRetry, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as stateBucketExistenceConfirmed, nt as readConfigString, o as planFailedOps, on as MIGRATE_TMP_PREFIX, ot as requireConfigString, p as IMPLICIT_DELETE_DEPENDENCIES, pn as AssemblyReader, pt as DiffCalculator, q as getAccountInfo, qn as markNonRetryable, qt as getDefaultStateBucketName, r as DeployEngine, rn as warnDeprecatedNoPrefixCliFlag, rt as replayWarn, s as planRollback, sn as findLargeInlineResources, st as s3BucketArn, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveUseCdkBootstrapAssets, tt as configStringRefusal, u as UNSPECIFIED_SKIP_REASON, un as PARTITION_TABLE, ut as s3BucketRegionalDomainName, v as ccRoutedFinalSnapshotError, vn as getAwsClients, vt as LockManager, w as extractDeploymentEventError, wt as WorkGraph, x as refusesFinalSnapshot, xt as shouldRetainResource, y as createPreDeleteFinalSnapshot, yn as resetAwsClients, yt as S3StateBackend, z as findActionableSilentDrops, zt as formatDockerLoginError } from "./deploy-engine-BdKa1DA4.js";
2
+ import { $ as coerceCfnBoolean, $t as resolveStateBucketWithDefault, A as cyan, An as MissingCdkCliError, At as AssetModeResolver, B as findSilentDropProperties, Bt as getDockerCmd, C as makeCanonicalizePropertiesFn, Ct as stringifyValue, D as renderStatefulReason, Dn as LocalMigrateError, Dt as loadPublishableAssetManifest, E as isStatefulRecreateTargetSync, En as LocalInvokeBuildError$1, Et as createAssetRedirectResolver, F as IAMRoleProvider, Fn as ResourceUpdateNotSupportedError, Ft as validateAssetBucketName, G as IntrinsicFunctionResolver, Gn as isRetryableTransientError, Gt as Synthesizer, H as slowCcOperationTimeoutMs, Hn as normalizeAwsError, Ht as runDockerStreaming, I as collectInlinePolicyNamesManagedBySiblings, In as StackHasActiveImportsError, It as validateContainerRepoName, J as refStateLookupFromResource, Jt as getLegacyStateBucketName, K as cfnRefValueFromPhysicalId, Kn as isThrottlingError, Kt as synthesisStatusMessage, L as clearOnUpdateRemoval, Ln as StackTerminationProtectionError, Lt as buildDenyExternalAccessPolicy, M as green, Mn as PartialFailureError, Mt as ensureAssetStorage, N as red, Nn as ProvisioningError, Nt as getBootstrapMarkerKey, O as formatResourceLine, On as LocalStartServiceError, Ot as rewriteTemplateAssetReferences, P as yellow, Pn as ResourceTimeoutError, Pt as parseBootstrapMarker, Q as assertRegionMatch, Qt as resolveSkipPrefix, R as ProviderRegistry, Rt as buildDockerImage, S as unsupportedFinalSnapshotError, Sn as CdkdError, St as AssetPublisher, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as DeployCancelledError, Tt as buildAssetRedirectMap, U as disableInstanceApiTermination, Un as withErrorHandling, Ut as AssetManifestLoader, V as CloudControlProvider, Vt as runDockerForeground, W as isTerminationProtectionPropagationError, Wn as isMarkedNonRetryable, Wt as getDockerImageBySourceHash, X as normalizeAwsTagsToCfn, Xt as resolveAutoAssetStorage, Y as WAFv2WebACLProvider, Yt as resolveApp, Z as resolveExplicitPhysicalId, Zt as resolveCaptureObservedState, _ as buildFinalSnapshotIdentifier, _n as AwsClients, _t as TemplateParser, a as DeploymentEventsStore, an as CFN_TEMPLATE_URL_LIMIT, at as requireConfigObject, b as isFinalSnapshotError, bn as setAwsClients, bt as rebuildClientForBucketRegion, c as replayFailedOperations, cn as uploadCfnTemplate, ct as s3BucketDomainName, d as deleteSkipReason, dn as canonicalizeRegion, dt as s3BucketWebsiteUrl, en as resolveStateBucketWithDefaultAndSource, et as configBooleanRefusal, f as withResourceDeadline, fn as derivePartitionAndUrlSuffix, ft as applyRoleArnIfSet, g as PRE_DELETE_SNAPSHOT_TYPES, gn as resolveBucketRegion, gt as DagBuilder, h as ATOMIC_FINAL_SNAPSHOT_TYPES, ht as withRetry, i as DeploymentEventsReader, in as CFN_TEMPLATE_BODY_LIMIT, it as requireConfigArray, j as gray, jn as NestedStackChildDirectDestroyError, jt as BOOTSTRAP_MARKER_PREFIX, k as bold, kt as escapeRegExp, l as replayRollback, ln as expectedOwnerParam, lt as s3BucketDualStackDomainName, m as computeImplicitDeleteEdges, mn as processStackMessages, mt as describeTypeWithThrottleRetry, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as stateBucketExistenceConfirmed, nt as readConfigString, o as planFailedOps, on as MIGRATE_TMP_PREFIX, ot as requireConfigString, p as IMPLICIT_DELETE_DEPENDENCIES, pn as AssemblyReader, pt as DiffCalculator, q as getAccountInfo, qn as markNonRetryable, qt as getDefaultStateBucketName, r as DeployEngine, rn as warnDeprecatedNoPrefixCliFlag, rt as replayWarn, s as planRollback, sn as findLargeInlineResources, st as s3BucketArn, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveUseCdkBootstrapAssets, tt as configStringRefusal, u as UNSPECIFIED_SKIP_REASON, un as PARTITION_TABLE, ut as s3BucketRegionalDomainName, v as ccRoutedFinalSnapshotError, vn as getAwsClients, vt as LockManager, w as extractDeploymentEventError, wt as WorkGraph, x as refusesFinalSnapshot, xt as shouldRetainResource, y as createPreDeleteFinalSnapshot, yn as resetAwsClients, yt as S3StateBackend, z as findActionableSilentDrops, zt as formatDockerLoginError } from "./deploy-engine-D-4ZvtGc.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-CrVgG0Pj.js";
4
+ import { t as ASGProvider } from "./asg-provider-Dl2MK3if.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";
@@ -2991,6 +2991,28 @@ var IAMPolicyProvider = class {
2991
2991
  //#endregion
2992
2992
  //#region src/provisioning/providers/iam-managed-policy-provider.ts
2993
2993
  /**
2994
+ * Matches an AWS-managed IAM policy ARN in ANY AWS partition (issue #1815).
2995
+ *
2996
+ * AWS-managed policies are the ones whose ACCOUNT segment is the literal
2997
+ * `aws` — `arn:<partition>:iam::aws:policy/<path><name>` — as opposed to a
2998
+ * customer-managed policy, which carries a 12-digit account id. They exist
2999
+ * under every partition (`arn:aws-us-gov:iam::aws:policy/AdministratorAccess`
3000
+ * is a real, attachable ARN in GovCloud), so pinning the PARTITION segment to
3001
+ * the commercial `aws` recognised only a third of them.
3002
+ *
3003
+ * The partition is read OFF THE ARN rather than derived from a region through
3004
+ * `derivePartitionAndUrlSuffix` (`src/utils/aws-partition.ts`): that helper
3005
+ * answers "given a region, which partition am I in", which is the right
3006
+ * question when BUILDING an ARN (the seven sites PR #1834 fixed) and the wrong
3007
+ * one when CLASSIFYING an ARN the caller already holds. The loose
3008
+ * `aws[a-z0-9-]*` partition segment matches `IAM_ROLE_ARN_RE` in
3009
+ * `src/utils/role-arn.ts`; a closed partition list is precisely what goes
3010
+ * stale when AWS adds a partition, and here staleness fails DANGEROUS (see the
3011
+ * refusal's own comment — an unrecognised AWS-managed policy gets adopted, and
3012
+ * destroy then detaches it from every principal in the account).
3013
+ */
3014
+ const AWS_MANAGED_POLICY_ARN_RE = /^arn:aws[a-z0-9-]*:iam::aws:/;
3015
+ /**
2994
3016
  * AWS IAM Managed Policy Provider
2995
3017
  *
2996
3018
  * Implements resource provisioning for AWS::IAM::ManagedPolicy using the IAM SDK.
@@ -3002,7 +3024,7 @@ var IAMPolicyProvider = class {
3002
3024
  * - Detach-before-delete cleanup (a ManagedPolicy with attached principals
3003
3025
  * or with non-default versions cannot be deleted directly)
3004
3026
  *
3005
- * Physical id is the policy ARN (`arn:aws:iam::<account>:policy/<path><name>`)
3027
+ * Physical id is the policy ARN (`arn:<partition>:iam::<account>:policy/<path><name>`)
3006
3028
  * since path is part of the identity — two policies with the same name in
3007
3029
  * different paths are distinct.
3008
3030
  */
@@ -3239,7 +3261,7 @@ var IAMManagedPolicyProvider = class {
3239
3261
  const explicit = resolveExplicitPhysicalId(input, "ManagedPolicyName");
3240
3262
  if (explicit) {
3241
3263
  if (explicit.startsWith("arn:")) {
3242
- if (explicit.startsWith("arn:aws:iam::aws:")) throw new Error(`Refusing to import AWS-managed policy ${explicit}: cdkd only adopts customer-managed policies. If you need to attach an AWS-managed policy to a role / user / group, reference it via ManagedPolicyArns on the principal instead.`);
3264
+ if (AWS_MANAGED_POLICY_ARN_RE.test(explicit)) throw new Error(`Refusing to import AWS-managed policy ${explicit}: cdkd only adopts customer-managed policies. If you need to attach an AWS-managed policy to a role / user / group, reference it via ManagedPolicyArns on the principal instead.`);
3243
3265
  try {
3244
3266
  await this.iamClient.send(new GetPolicyCommand({ PolicyArn: explicit }));
3245
3267
  return {
@@ -3468,7 +3490,7 @@ var IAMManagedPolicyProvider = class {
3468
3490
  }
3469
3491
  };
3470
3492
  /**
3471
- * Recover the policy name from `arn:aws:iam::<account>:policy/<path><name>`.
3493
+ * Recover the policy name from `arn:<partition>:iam::<account>:policy/<path><name>`.
3472
3494
  * Used to decide whether `ManagedPolicyName` was mutated relative to the
3473
3495
  * physical id we recorded — name + path are immutable on AWS, so any
3474
3496
  * difference is a replacement signal.
@@ -12354,6 +12376,47 @@ var LambdaUrlProvider = class {
12354
12376
 
12355
12377
  //#endregion
12356
12378
  //#region src/provisioning/providers/lambda-eventsource-provider.ts
12379
+ /**
12380
+ * Captures the SERVICE segment of an ARN in ANY AWS partition (issue #1815).
12381
+ *
12382
+ * `arn:<partition>:<service>:<region>:<account>:<rest>` — group 1 is the
12383
+ * service. The partition segment is matched loosely as `aws[a-z0-9-]*`, the
12384
+ * same shape `IAM_ROLE_ARN_RE` (`src/utils/role-arn.ts`) uses, rather than
12385
+ * enumerated: this classifier previously hand-listed `aws` and `aws-cn` per
12386
+ * service and so returned `'unknown'` for every OTHER partition — GovCloud,
12387
+ * all four iso partitions and `aws-eusc` — which silently disabled the
12388
+ * type-discriminator gating below (see the `KINDS_WITH_*` sets) and let
12389
+ * `cdkd drift --revert` push `FunctionResponseTypes` / `SourceAccessConfigurations`
12390
+ * placeholders AWS rejects. A closed partition list is exactly what goes stale
12391
+ * when AWS adds a partition; reading the partition off the ARN cannot.
12392
+ *
12393
+ * Note the partition is not derived from a region via
12394
+ * `derivePartitionAndUrlSuffix` (`src/utils/aws-partition.ts`) because this
12395
+ * CLASSIFIES an ARN already in hand — the partition is in the input, and no
12396
+ * region reaches this function.
12397
+ */
12398
+ const ARN_SERVICE_RE = /^arn:aws[a-z0-9-]*:([^:]+):/;
12399
+ /**
12400
+ * ARN service segment -> {@link EventSourceKind}. Replaces the per-partition
12401
+ * `startsWith` chain; every entry is partition-independent by construction.
12402
+ */
12403
+ const ARN_SERVICE_TO_EVENT_SOURCE_KIND = {
12404
+ sqs: "sqs",
12405
+ kinesis: "kinesis",
12406
+ dynamodb: "dynamodb",
12407
+ kafka: "kafka",
12408
+ mq: "mq",
12409
+ rds: "documentdb"
12410
+ };
12411
+ /**
12412
+ * @internal EXPORTED for tests only (issue #1815). Every production consumer is a
12413
+ * `Set.has(kind)`, so the `Object.hasOwn` guard below is UNOBSERVABLE through
12414
+ * behavior — a prototype member and `'unknown'` both miss every set. That was
12415
+ * measured, not assumed: a behavior-level test for it passed with the guard
12416
+ * reverted. Rather than ship an unverifiable guard or a test that only looks
12417
+ * like one, the classifier's RETURN VALUE is asserted directly. Do not read the
12418
+ * export as an invitation to call this from outside the provider.
12419
+ */
12357
12420
  function classifyEventSource(resp) {
12358
12421
  if (resp.SelfManagedEventSource !== void 0) return "kafka";
12359
12422
  if (resp.SelfManagedKafkaEventSourceConfig !== void 0) return "kafka";
@@ -12361,13 +12424,9 @@ function classifyEventSource(resp) {
12361
12424
  if (resp.DocumentDBEventSourceConfig !== void 0) return "documentdb";
12362
12425
  const arn = resp.EventSourceArn;
12363
12426
  if (!arn) return "unknown";
12364
- if (arn.startsWith("arn:aws:sqs:") || arn.startsWith("arn:aws-cn:sqs:")) return "sqs";
12365
- if (arn.startsWith("arn:aws:kinesis:") || arn.startsWith("arn:aws-cn:kinesis:")) return "kinesis";
12366
- if (arn.startsWith("arn:aws:dynamodb:") || arn.startsWith("arn:aws-cn:dynamodb:")) return "dynamodb";
12367
- if (arn.startsWith("arn:aws:kafka:") || arn.startsWith("arn:aws-cn:kafka:")) return "kafka";
12368
- if (arn.startsWith("arn:aws:mq:") || arn.startsWith("arn:aws-cn:mq:")) return "mq";
12369
- if (arn.startsWith("arn:aws:rds:") || arn.startsWith("arn:aws-cn:rds:")) return "documentdb";
12370
- return "unknown";
12427
+ const service = ARN_SERVICE_RE.exec(arn)?.[1];
12428
+ if (!service) return "unknown";
12429
+ return Object.hasOwn(ARN_SERVICE_TO_EVENT_SOURCE_KIND, service) ? ARN_SERVICE_TO_EVENT_SOURCE_KIND[service] : "unknown";
12371
12430
  }
12372
12431
  /**
12373
12432
  * Classify an event source mapping from its CFn property bag (as opposed to
@@ -55372,7 +55431,10 @@ var NestedStackProvider = class {
55372
55431
  ...ctx.destroyOptions?.resourceWarnAfterByType && { resourceWarnAfterByType: ctx.destroyOptions.resourceWarnAfterByType },
55373
55432
  ...ctx.destroyOptions?.resourceTimeoutByType && { resourceTimeoutByType: ctx.destroyOptions.resourceTimeoutByType }
55374
55433
  }));
55375
- if (childResult.errorCount > 0) throw new Error(nestedStackChildFailureMessage(childStackName, childResult.errorCount, childResult.skippedCount, childResult.interrupted));
55434
+ if (childResult.errorCount > 0) {
55435
+ const failure = new Error(nestedStackChildFailureMessage(childStackName, childResult.errorCount, childResult.skippedCount, childResult.interrupted));
55436
+ throw childResult.interrupted ? markNonRetryable(failure) : failure;
55437
+ }
55376
55438
  if (childResult.skippedCount > 0 || childResult.interrupted) {
55377
55439
  const causes = [];
55378
55440
  if (childResult.skippedCount > 0) causes.push(`skipped ${childResult.skippedCount} resource(s)`);
@@ -55410,7 +55472,7 @@ var NestedStackProvider = class {
55410
55472
  }
55411
55473
  requireContext() {
55412
55474
  const ctx = getCurrentNestedStackContext();
55413
- if (!ctx) throw new Error("NestedStackProvider invoked outside withNestedStackContext() scope. The deploy / destroy CLI entry point must wrap its DeployEngine.deploy / runDestroyForStack call in withNestedStackContext(ctx, () => ...).");
55475
+ if (!ctx) throw markNonRetryable(/* @__PURE__ */ new Error("NestedStackProvider invoked outside withNestedStackContext() scope. The deploy / destroy CLI entry point must wrap its DeployEngine.deploy / runDestroyForStack call in withNestedStackContext(ctx, () => ...)."));
55414
55476
  return ctx;
55415
55477
  }
55416
55478
  requireDeployContext(ctx, op) {
@@ -68858,12 +68920,21 @@ function indexGrandchildTemplatePaths(childTemplate, childTemplatePath) {
68858
68920
  * `--stack-region`, bucket resolution failure) logs at warn and returns
68859
68921
  * `undefined`. Auth failures and other genuine errors propagate.
68860
68922
  *
68861
- * Read-only against state no lock acquisition or save path here.
68923
+ * No lock acquisition and no `state.json` write path here. That is NOT the
68924
+ * same as "read-only against the bucket", and the difference was mis-stated
68925
+ * until issue #1836: {@link buildCrossStackResolver}'s exports-index lookup
68926
+ * goes through {@link ExportIndexStore}, whose `load` REBUILDS the index from
68927
+ * the per-stack state files on a miss and PUTs the rebuilt object
68928
+ * (`cdkd/_index/{region}/exports.json`). So an `Fn::ImportValue` lookup from a
68929
+ * `cdkd local` command can write that one derived key. Keeping the index KEY
68930
+ * the same spelling the state records carry is what keeps that write CORRECT
68931
+ * rather than an empty index — see the note on `consumerRegion` below.
68862
68932
  */
68863
68933
  async function loadStateForStack(stackName, synthRegion, opts) {
68864
68934
  const logger = getLogger();
68865
68935
  const prefix = opts.logPrefix ?? "--from-state";
68866
- const region = opts.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? synthRegion ?? "us-east-1";
68936
+ const region = canonicalizeRegion(opts.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? synthRegion ?? "us-east-1");
68937
+ const clientRegion = canonicalizeRegion(opts.region) || void 0;
68867
68938
  let stateBucket;
68868
68939
  try {
68869
68940
  stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, region);
@@ -68872,7 +68943,7 @@ async function loadStateForStack(stackName, synthRegion, opts) {
68872
68943
  return;
68873
68944
  }
68874
68945
  const awsClients = new AwsClients({
68875
- ...opts.region !== void 0 && { region: opts.region },
68946
+ ...clientRegion !== void 0 && { region: clientRegion },
68876
68947
  ...opts.profile !== void 0 && { profile: opts.profile }
68877
68948
  });
68878
68949
  setAwsClients(awsClients);
@@ -68882,7 +68953,7 @@ async function loadStateForStack(stackName, synthRegion, opts) {
68882
68953
  prefix: opts.statePrefix
68883
68954
  };
68884
68955
  const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
68885
- ...opts.region !== void 0 && { region: opts.region },
68956
+ ...clientRegion !== void 0 && { region: clientRegion },
68886
68957
  ...opts.profile !== void 0 && { profile: opts.profile }
68887
68958
  });
68888
68959
  await stateBackend.verifyBucketExists();
@@ -68891,15 +68962,58 @@ async function loadStateForStack(stackName, synthRegion, opts) {
68891
68962
  logger.warn(`${prefix}: no cdkd state found for stack '${stackName}' in bucket '${stateBucket}'. Was it deployed via 'cdkd deploy'? Falling back.`);
68892
68963
  return;
68893
68964
  }
68965
+ /**
68966
+ * Issue #1836: EXACT match first, case-insensitive second.
68967
+ *
68968
+ * `S3StateBackend.listStacks` dedupes on the EXACT `{stack}\0{region}` pair,
68969
+ * so `cdkd/MyStack/US-EAST-1/state.json` and
68970
+ * `cdkd/MyStack/us-east-1/state.json` are two DISTINCT refs — and
68971
+ * ListObjectsV2 returns them in ASCII order, i.e. the upper-cased one FIRST.
68972
+ * A fold-only `find` therefore hands `--stack-region us-east-1` the OTHER
68973
+ * record, silently reading state the user did not name; the pre-fold `===`
68974
+ * got that case right. So the fold is the RECOVERY for a case mismatch, never
68975
+ * an override of a spelling that exists verbatim.
68976
+ *
68977
+ * `candidate` is therefore the spelling the USER supplied, not the folded
68978
+ * one (round 3 of the #1836 review): the fold at each command's handler
68979
+ * entry and at the `--from-state` factory is what an SDK client needs, but
68980
+ * feeding it to this compare made the exact-match rule unreachable from any
68981
+ * real CLI invocation — every candidate was already canonical, so `exact`
68982
+ * could only ever match an already-canonical record and `--stack-region
68983
+ * US-EAST-1` read the `us-east-1` one while REPORTING it as the exact
68984
+ * spelling. `opts.rawStackRegion` carries the unfolded value here; the
68985
+ * recovery arm folds both sides itself, so nothing upstream has to.
68986
+ *
68987
+ * When more than one ref is canonical-equal the choice is announced, because
68988
+ * either answer reads some record the user did not spell out in full and the
68989
+ * silence is what made the original miss hard to diagnose. The wording
68990
+ * states which of the two rules decided it — and does so from the same
68991
+ * `exact` binding the choice was made from, so it cannot claim an exact
68992
+ * match the compare did not make.
68993
+ */
68994
+ const findByRegion = (candidate, source) => {
68995
+ const exact = refs.find((r) => r.region === candidate);
68996
+ const folded = refs.filter((r) => canonicalizeRegion(r.region) === canonicalizeRegion(candidate));
68997
+ const chosen = exact ?? folded[0];
68998
+ if (folded.length > 1 && chosen) {
68999
+ const how = exact ? `matches ${source} '${candidate}' exactly` : `no record spells ${source} '${candidate}' exactly, so this is a case-insensitive recovery`;
69000
+ logger.warn(`${prefix}: stack '${stackName}' has state under ${folded.length} case-variant spellings of ${source} '${candidate}' (${folded.map((r) => r.region ?? "(legacy)").join(", ")}). Reading '${chosen.region ?? "(legacy)"}' — ${how}.`);
69001
+ }
69002
+ return chosen;
69003
+ };
69004
+ let synthMatch;
69005
+ if (!opts.stackRegion && synthRegion) synthMatch = findByRegion(synthRegion, "the synth-derived stack region");
68894
69006
  let targetRegion;
68895
69007
  if (opts.stackRegion) {
68896
- if (!refs.find((r) => r.region === opts.stackRegion)) {
69008
+ const requestedRegion = opts.rawStackRegion ?? opts.stackRegion;
69009
+ const found = findByRegion(requestedRegion, "--stack-region");
69010
+ if (!found) {
68897
69011
  const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
68898
- logger.warn(`${prefix}: stack '${stackName}' has no state in region '${opts.stackRegion}' (available: ${seen}). Falling back.`);
69012
+ logger.warn(`${prefix}: stack '${stackName}' has no state in region '${requestedRegion}' (available: ${seen}). Falling back.`);
68899
69013
  return;
68900
69014
  }
68901
- targetRegion = opts.stackRegion;
68902
- } else if (synthRegion && refs.some((r) => r.region === synthRegion)) targetRegion = synthRegion;
69015
+ targetRegion = found.region ?? requestedRegion;
69016
+ } else if (synthRegion && synthMatch) targetRegion = synthMatch.region ?? synthRegion;
68903
69017
  else if (refs.length === 1) targetRegion = refs[0].region ?? synthRegion ?? region;
68904
69018
  else {
68905
69019
  const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
@@ -68948,7 +69062,8 @@ async function loadStateForStack(stackName, synthRegion, opts) {
68948
69062
  async function loadBootstrapContainerRepo(synthRegion, opts) {
68949
69063
  const logger = getLogger();
68950
69064
  const prefix = opts.logPrefix ?? "--from-state";
68951
- const region = opts.region ?? opts.stackRegion ?? synthRegion ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? "us-east-1";
69065
+ const rawRegion = opts.region ?? opts.rawStackRegion ?? opts.stackRegion ?? synthRegion ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? "us-east-1";
69066
+ const region = canonicalizeRegion(rawRegion);
68952
69067
  let stateBucket;
68953
69068
  try {
68954
69069
  stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, region);
@@ -68956,8 +69071,9 @@ async function loadBootstrapContainerRepo(synthRegion, opts) {
68956
69071
  logger.debug(`${prefix}: could not resolve state bucket for the bootstrap-marker read: ${err instanceof Error ? err.message : String(err)}. Falling back to conventional asset-repo names.`);
68957
69072
  return;
68958
69073
  }
69074
+ const clientRegion = canonicalizeRegion(opts.region) || void 0;
68959
69075
  const awsClients = new AwsClients({
68960
- ...opts.region !== void 0 && { region: opts.region },
69076
+ ...clientRegion !== void 0 && { region: clientRegion },
68961
69077
  ...opts.profile !== void 0 && { profile: opts.profile }
68962
69078
  });
68963
69079
  setAwsClients(awsClients);
@@ -68967,16 +69083,25 @@ async function loadBootstrapContainerRepo(synthRegion, opts) {
68967
69083
  prefix: opts.statePrefix
68968
69084
  };
68969
69085
  const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
68970
- ...opts.region !== void 0 && { region: opts.region },
69086
+ ...clientRegion !== void 0 && { region: clientRegion },
68971
69087
  ...opts.profile !== void 0 && { profile: opts.profile }
68972
69088
  });
68973
69089
  const markerKey = getBootstrapMarkerKey(region);
68974
- const body = await stateBackend.getRawObject(markerKey);
69090
+ let resolvedKey = markerKey;
69091
+ let body = await stateBackend.getRawObject(markerKey);
69092
+ if (body === null && rawRegion !== region) {
69093
+ const rawKey = getBootstrapMarkerKey(rawRegion);
69094
+ body = await stateBackend.getRawObject(rawKey);
69095
+ if (body !== null) {
69096
+ resolvedKey = rawKey;
69097
+ logger.debug(`${prefix}: bootstrap marker found at the un-folded key '${rawKey}' (no '${markerKey}') — an upper-cased region was used at 'cdkd bootstrap' time.`);
69098
+ }
69099
+ }
68975
69100
  if (body === null) {
68976
- logger.debug(`${prefix}: no bootstrap marker at '${markerKey}' in bucket '${stateBucket}' — assuming conventional asset-repo names.`);
69101
+ logger.debug(`${prefix}: no bootstrap marker at '${markerKey}'${rawRegion !== region ? ` (nor '${getBootstrapMarkerKey(rawRegion)}')` : ""} in bucket '${stateBucket}' — assuming conventional asset-repo names.`);
68977
69102
  return;
68978
69103
  }
68979
- const marker = parseBootstrapMarker(body, markerKey);
69104
+ const marker = parseBootstrapMarker(body, resolvedKey);
68980
69105
  logger.debug(`${prefix}: bootstrap marker for ${region} names container repo '${marker.containerRepo}'.`);
68981
69106
  return marker.containerRepo;
68982
69107
  } catch (err) {
@@ -69015,13 +69140,14 @@ async function buildCrossStackResolver(consumerRegion, opts) {
69015
69140
  const prefix = opts.logPrefix ?? "--from-state";
69016
69141
  let stateBucket;
69017
69142
  try {
69018
- stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, consumerRegion);
69143
+ stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, canonicalizeRegion(consumerRegion));
69019
69144
  } catch (err) {
69020
69145
  logger.warn(`${prefix}: cross-stack resolver could not resolve state bucket: ${err instanceof Error ? err.message : String(err)}. Fn::ImportValue / Fn::GetStackOutput env entries will warn-and-drop.`);
69021
69146
  return;
69022
69147
  }
69148
+ const clientRegion = canonicalizeRegion(opts.region) || void 0;
69023
69149
  const awsClients = new AwsClients({
69024
- ...opts.region !== void 0 && { region: opts.region },
69150
+ ...clientRegion !== void 0 && { region: clientRegion },
69025
69151
  ...opts.profile !== void 0 && { profile: opts.profile }
69026
69152
  });
69027
69153
  const stateConfig = {
@@ -69029,7 +69155,7 @@ async function buildCrossStackResolver(consumerRegion, opts) {
69029
69155
  prefix: opts.statePrefix
69030
69156
  };
69031
69157
  const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
69032
- ...opts.region !== void 0 && { region: opts.region },
69158
+ ...clientRegion !== void 0 && { region: clientRegion },
69033
69159
  ...opts.profile !== void 0 && { profile: opts.profile }
69034
69160
  });
69035
69161
  try {
@@ -69061,9 +69187,10 @@ async function buildCrossStackResolver(consumerRegion, opts) {
69061
69187
  logger.debug(`${prefix}: failed to list stacks during Fn::ImportValue fallback for '${exportName}': ${err instanceof Error ? err.message : String(err)}`);
69062
69188
  return;
69063
69189
  }
69190
+ const canonicalConsumerRegion = canonicalizeRegion(consumerRegion);
69064
69191
  for (const ref of refs) {
69065
69192
  const region = ref.region ?? consumerRegion;
69066
- if (region !== consumerRegion) continue;
69193
+ if (canonicalizeRegion(region) !== canonicalConsumerRegion) continue;
69067
69194
  try {
69068
69195
  const got = await stateBackend.getState(ref.stackName, region);
69069
69196
  if (!got || !got.state.outputs) continue;
@@ -69080,14 +69207,31 @@ async function buildCrossStackResolver(consumerRegion, opts) {
69080
69207
  }
69081
69208
  },
69082
69209
  async resolveGetStackOutput(producerStack, producerRegion, outputName) {
69083
- try {
69084
- const got = await stateBackend.getState(producerStack, producerRegion);
69210
+ const readOutput = (got) => {
69085
69211
  if (!got || !got.state.outputs) return void 0;
69086
69212
  if (!(outputName in got.state.outputs)) return void 0;
69087
69213
  const value = got.state.outputs[outputName];
69088
69214
  if (typeof value === "string") return value;
69089
69215
  if (typeof value === "number" || typeof value === "boolean") return String(value);
69090
69216
  return JSON.stringify(value);
69217
+ };
69218
+ try {
69219
+ const got = await stateBackend.getState(producerStack, producerRegion);
69220
+ if (got) return readOutput(got);
69221
+ const canonicalProducerRegion = canonicalizeRegion(producerRegion);
69222
+ const refs = await stateBackend.listStacks();
69223
+ for (const ref of refs) {
69224
+ if (ref.stackName !== producerStack) continue;
69225
+ const region = ref.region ?? producerRegion;
69226
+ if (region === producerRegion) continue;
69227
+ if (canonicalizeRegion(region) !== canonicalProducerRegion) continue;
69228
+ const recovered = readOutput(await stateBackend.getState(producerStack, region));
69229
+ if (recovered !== void 0) {
69230
+ logger.debug(`${prefix}: Fn::GetStackOutput '${producerStack}.${outputName}' resolved from the case-variant state record '${region}' (no record spells '${producerRegion}' exactly).`);
69231
+ return recovered;
69232
+ }
69233
+ }
69234
+ return;
69091
69235
  } catch (err) {
69092
69236
  logger.debug(`${prefix}: state read failed for Fn::GetStackOutput '${producerStack}.${outputName}' (${producerRegion}): ${err instanceof Error ? err.message : String(err)}`);
69093
69237
  return;
@@ -69127,6 +69271,7 @@ var S3LocalStateProvider = class {
69127
69271
  const loaded = await loadStateForStack(stackName, synthRegion, {
69128
69272
  statePrefix: this.opts.statePrefix,
69129
69273
  ...this.opts.stackRegion !== void 0 && { stackRegion: this.opts.stackRegion },
69274
+ ...this.opts.rawStackRegion !== void 0 && { rawStackRegion: this.opts.rawStackRegion },
69130
69275
  ...this.opts.stateBucket !== void 0 && { stateBucket: this.opts.stateBucket },
69131
69276
  ...this.opts.region !== void 0 && { region: this.opts.region },
69132
69277
  ...this.opts.profile !== void 0 && { profile: this.opts.profile }
@@ -69193,12 +69338,16 @@ var S3LocalStateProvider = class {
69193
69338
  */
69194
69339
  const fromStateFactory = (options) => {
69195
69340
  const opts = options;
69341
+ const clientRegion = canonicalizeRegion(opts.region) || void 0;
69342
+ const stackRegion = canonicalizeRegion(opts.stackRegion) || void 0;
69343
+ const rawStackRegion = opts.rawStackRegion || opts.stackRegion || void 0;
69196
69344
  return new S3LocalStateProvider({
69197
69345
  statePrefix: opts.statePrefix,
69198
69346
  ...opts.stateBucket !== void 0 && { stateBucket: opts.stateBucket },
69199
- ...opts.region !== void 0 && { region: opts.region },
69347
+ ...clientRegion !== void 0 && { region: clientRegion },
69200
69348
  ...opts.profile !== void 0 && { profile: opts.profile },
69201
- ...opts.stackRegion !== void 0 && { stackRegion: opts.stackRegion }
69349
+ ...stackRegion !== void 0 && { stackRegion },
69350
+ ...rawStackRegion !== void 0 && { rawStackRegion }
69202
69351
  });
69203
69352
  };
69204
69353
  /**
@@ -72380,6 +72529,10 @@ async function localStartApiCommand(target, options) {
72380
72529
  const logger = getLogger();
72381
72530
  if (options.verbose) logger.setLevel("debug");
72382
72531
  if (options.region !== void 0) options.region = canonicalizeRegion(options.region);
72532
+ if (options.stackRegion !== void 0) {
72533
+ options.rawStackRegion = options.stackRegion;
72534
+ options.stackRegion = canonicalizeRegion(options.stackRegion);
72535
+ }
72383
72536
  let apiFilter = target;
72384
72537
  if (options.api !== void 0) {
72385
72538
  if (target !== void 0) throw new Error(`Cannot specify both positional target ('${target}') and --api flag ('${options.api}'). Use one or the other. The positional form is preferred — '--api' is a deprecated alias.`);
@@ -74722,6 +74875,10 @@ async function localRunTaskCommand(target, options) {
74722
74875
  if (options.verbose) logger.setLevel("debug");
74723
74876
  warnIfDeprecatedRegion(options);
74724
74877
  if (options.region !== void 0) options.region = canonicalizeRegion(options.region);
74878
+ if (options.stackRegion !== void 0) {
74879
+ options.rawStackRegion = options.stackRegion;
74880
+ options.stackRegion = canonicalizeRegion(options.stackRegion);
74881
+ }
74725
74882
  const state = createEcsRunState();
74726
74883
  let sigintHandler;
74727
74884
  let sigintCount = 0;
@@ -74766,12 +74923,12 @@ async function localRunTaskCommand(target, options) {
74766
74923
  const { stacks } = await synthesizer.synthesize(synthOpts);
74767
74924
  const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
74768
74925
  stateProvider = createLocalStateProvider$1(options, candidate?.stackName ?? "", candidate?.region);
74769
- const imageContext = await buildEcsImageResolutionContext$1(candidate, stateProvider, options);
74926
+ const { context: imageContext, stateRecordRegion } = await buildEcsImageResolutionContext$1(candidate, stateProvider, options);
74770
74927
  const task = resolveEcsTaskTarget(target, stacks, imageContext);
74771
74928
  logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
74772
74929
  const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
74773
74930
  if (stateProvider && taskNeeds.needsCrossStackResolver) {
74774
- const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? task.stack.region ?? "us-east-1";
74931
+ const consumerRegion = resolveEcsConsumerRegion(stateRecordRegion, options, task.stack.region);
74775
74932
  const resolver = await stateProvider.buildCrossStackResolver(consumerRegion);
74776
74933
  if (resolver) await applyCrossStackResolverToTask(task, {
74777
74934
  resources: imageContext?.stateResources ?? {},
@@ -74881,9 +75038,48 @@ async function assumeTaskRole(roleArn, region) {
74881
75038
  }
74882
75039
  }
74883
75040
  /**
75041
+ * Region the cross-stack resolver is scoped to for `cdkd local run-task`
75042
+ * (issue #1836).
75043
+ *
75044
+ * This value does THREE things (round 3 said TWO and was wrong — corrected in
75045
+ * round 4), and the first is why the state record's own spelling outranks the env
75046
+ * chain:
75047
+ *
75048
+ * 1. it becomes the exports-index KEY `cdkd/_index/{region}/exports.json`;
75049
+ * 2. it is the same-region filter of the index-miss per-stack scan; and
75050
+ * 3. via `SubstitutionContext.consumerRegion`, it is cdk-local's DEFAULT producer
75051
+ * region for an `Fn::GetStackOutput` that carries no explicit `Region` — i.e.
75052
+ * it becomes the `cdkd/{producer}/{region}/state.json` key that intrinsic
75053
+ * reads.
75054
+ *
75055
+ * All three land in `buildCrossStackResolver` (`local-state-loader.ts`), whose
75056
+ * `resolveImport` scan (2) and — since round 4 — `resolveGetStackOutput` (3) fold
75057
+ * BOTH sides for the comparison / recovery, while (1) is deliberately raw.
75058
+ *
75059
+ * `local invoke` / `local invoke-agentcore` both pass `loaded.region` there; this
75060
+ * command passed a FOLDED env-chain value instead, so the two commands keyed one
75061
+ * index differently — and a key the state records do not spell makes
75062
+ * `ExportIndexStore`'s rebuild filter match zero refs and PUT an EMPTY index,
75063
+ * permanently degrading every `Fn::ImportValue` to the O(N) scan. Keyed by a
75064
+ * record's spelling the filter always matches.
75065
+ *
75066
+ * The env-chain arm remains for the case where no state record was loaded at all
75067
+ * (nothing in the task needed one), and it stays FOLDED for the reason the rest
75068
+ * of this file folds: both env-var links escape the handler-entry `--region`
75069
+ * fold, and a raw upper-cased spelling reaches an S3 key and an SDK client.
75070
+ *
75071
+ * @internal exported for unit tests.
75072
+ */
75073
+ function resolveEcsConsumerRegion(stateRecordRegion, options, synthRegion) {
75074
+ if (stateRecordRegion) return stateRecordRegion;
75075
+ return canonicalizeRegion((options.region || process.env["AWS_REGION"]) ?? process.env["AWS_DEFAULT_REGION"] ?? synthRegion ?? "us-east-1");
75076
+ }
75077
+ /**
74884
75078
  * Build the substitution context the ECS task resolver consumes (issue
74885
- * #264). Returns `undefined` when no container's `Image` field needs
74886
- * substitution the resolver behaves as before in that case.
75079
+ * #264), plus the region of the state record it loaded (if any — issue #1836;
75080
+ * see {@link resolveEcsConsumerRegion} for what reads it). `context` is
75081
+ * `undefined` when no container's `Image` field needs substitution — the
75082
+ * resolver behaves as before in that case.
74887
75083
  *
74888
75084
  * Tier 1 (pseudo parameters) fires `sts:GetCallerIdentity` once for
74889
75085
  * `${AWS::AccountId}`; region / partition / URL suffix come from the CLI
@@ -74902,10 +75098,11 @@ async function assumeTaskRole(roleArn, region) {
74902
75098
  */
74903
75099
  async function buildEcsImageResolutionContext$1(candidate, stateProvider, options) {
74904
75100
  const logger = getLogger();
74905
- if (!candidate) return void 0;
75101
+ if (!candidate) return {};
74906
75102
  const needs = detectEcsImageResolutionNeeds(candidate);
74907
- if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution && !needs.needsAssetRepoMarker) return;
75103
+ if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution && !needs.needsAssetRepoMarker) return {};
74908
75104
  const ctx = {};
75105
+ let stateRecordRegion;
74909
75106
  const wantsPseudoForEnvOrSecret = !!stateProvider && needs.needsEnvOrSecretSubstitution;
74910
75107
  if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
74911
75108
  const region = canonicalizeRegion(options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region);
@@ -74929,13 +75126,17 @@ async function buildEcsImageResolutionContext$1(candidate, stateProvider, option
74929
75126
  const wantsState = needs.needsStateResources || needs.needsEnvOrSecretSubstitution;
74930
75127
  if (stateProvider && wantsState) {
74931
75128
  const loaded = await stateProvider.load(candidate.stackName, candidate.region);
74932
- if (loaded) ctx.stateResources = loaded.resources;
75129
+ if (loaded) {
75130
+ ctx.stateResources = loaded.resources;
75131
+ stateRecordRegion = loaded.region;
75132
+ }
74933
75133
  } else if (!stateProvider && needs.needsStateResources) logger.warn("Container Image references a same-stack AWS::ECR::Repository. Pass --from-state (cdkd-deployed) or --from-cfn-stack (cdk-deployed) to substitute the deployed repository URI. Otherwise the resolver will surface its existing error.");
74934
75134
  else if (!stateProvider && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics (Ref / Fn::GetAtt / Fn::Sub / Fn::Join). Pass --from-state (cdkd-deployed) or --from-cfn-stack (cdk-deployed) to substitute them against deployed state. Without a state source these entries are dropped (per-key warnings will follow).");
74935
75135
  if (needs.needsAssetRepoMarker && options.fromState) {
74936
75136
  const containerRepo = await loadBootstrapContainerRepo(candidate.region, {
74937
75137
  statePrefix: options.statePrefix,
74938
75138
  ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
75139
+ ...options.rawStackRegion !== void 0 && { rawStackRegion: options.rawStackRegion },
74939
75140
  ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
74940
75141
  ...options.region !== void 0 && { region: options.region },
74941
75142
  ...options.profile !== void 0 && { profile: options.profile },
@@ -74943,7 +75144,10 @@ async function buildEcsImageResolutionContext$1(candidate, stateProvider, option
74943
75144
  });
74944
75145
  if (containerRepo !== void 0) ctx.cdkAssetContainerRepo = containerRepo;
74945
75146
  }
74946
- return ctx;
75147
+ return {
75148
+ context: ctx,
75149
+ ...stateRecordRegion !== void 0 && { stateRecordRegion }
75150
+ };
74947
75151
  }
74948
75152
  function pickCandidateStack(stackPattern, stacks) {
74949
75153
  if (stackPattern === null) {
@@ -75376,6 +75580,10 @@ async function localInvokeAgentCoreCommand(target, options) {
75376
75580
  if (options.verbose) logger.setLevel("debug");
75377
75581
  warnIfDeprecatedRegion(options);
75378
75582
  if (options.region !== void 0) options.region = canonicalizeRegion(options.region);
75583
+ if (options.stackRegion !== void 0) {
75584
+ options.rawStackRegion = options.stackRegion;
75585
+ options.stackRegion = canonicalizeRegion(options.stackRegion);
75586
+ }
75379
75587
  let containerId;
75380
75588
  let stopLogs;
75381
75589
  let sigintHandler;
@@ -76266,16 +76474,37 @@ function emitA2aResult(result) {
76266
76474
  function platformToArchitecture(platform) {
76267
76475
  return platform === "linux/amd64" ? "x86_64" : "arm64";
76268
76476
  }
76477
+ /**
76478
+ * Copy the developer's AWS credentials + region into the container env for the
76479
+ * DEFAULT (no `--assume-role`) path.
76480
+ *
76481
+ * Issue [#1836](https://github.com/go-to-k/cdkd/issues/1836): the two REGION
76482
+ * entries are folded through `canonicalizeRegion` on the way in; the credentials
76483
+ * are copied verbatim (an AKID / session token is case-SENSITIVE, so lower-casing
76484
+ * the whole pass-through list would corrupt every one of them). The value lands
76485
+ * in the container's own `AWS_REGION`, i.e. in every SDK client the agent builds,
76486
+ * and SDK endpoint resolution is case-SENSITIVE (`CN-NORTH-1` resolves the
76487
+ * COMMERCIAL partition).
76488
+ *
76489
+ * Folding only inside `applyAgentCoreCredentialEnv`'s assume-role arm — which is
76490
+ * where issue #1814 stopped, and what round 3 of the #1836 review found still
76491
+ * open here after the same gap was closed in `local-invoke.ts` — made ONE command
76492
+ * yield two different container regions for the same shell: `cn-north-1` with
76493
+ * `--assume-role`, `CN-NORTH-1` without.
76494
+ */
76269
76495
  function forwardAwsEnv$1(env) {
76270
- for (const key of [
76496
+ const passThrough = [
76271
76497
  "AWS_ACCESS_KEY_ID",
76272
76498
  "AWS_SECRET_ACCESS_KEY",
76273
76499
  "AWS_SESSION_TOKEN",
76274
76500
  "AWS_REGION",
76275
76501
  "AWS_DEFAULT_REGION"
76276
- ]) {
76502
+ ];
76503
+ const regionKeys = /* @__PURE__ */ new Set(["AWS_REGION", "AWS_DEFAULT_REGION"]);
76504
+ for (const key of passThrough) {
76277
76505
  const value = process.env[key];
76278
- if (value !== void 0) env[key] = value;
76506
+ if (value === void 0) continue;
76507
+ env[key] = regionKeys.has(key) ? canonicalizeRegion(value) : value;
76279
76508
  }
76280
76509
  }
76281
76510
  async function assumeAgentCoreExecutionRole(roleArn, region) {
@@ -76578,6 +76807,10 @@ async function localInvokeCommand(target, options) {
76578
76807
  if (options.verbose) logger.setLevel("debug");
76579
76808
  warnIfDeprecatedRegion(options);
76580
76809
  if (options.region !== void 0) options.region = canonicalizeRegion(options.region);
76810
+ if (options.stackRegion !== void 0) {
76811
+ options.rawStackRegion = options.stackRegion;
76812
+ options.stackRegion = canonicalizeRegion(options.stackRegion);
76813
+ }
76581
76814
  let imagePlan;
76582
76815
  let containerId;
76583
76816
  let stopLogs;
@@ -76731,29 +76964,12 @@ async function localInvokeCommand(target, options) {
76731
76964
  AWS_LAMBDA_LOG_STREAM_NAME: "local",
76732
76965
  ...envResult.resolved
76733
76966
  };
76734
- let assumeSucceeded = false;
76735
- if (resolvedAssumeRoleArn) {
76736
- const stsRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
76737
- try {
76738
- const creds = await assumeLambdaExecutionRole(resolvedAssumeRoleArn, stsRegion);
76739
- dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
76740
- dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
76741
- dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
76742
- if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
76743
- assumeSucceeded = true;
76744
- } catch (err) {
76745
- const reason = err instanceof Error ? err.message : String(err);
76746
- logger.warn(`--assume-role: STS AssumeRole(${resolvedAssumeRoleArn}) failed: ${reason}. Falling back to the developer's shell credentials.`);
76747
- }
76748
- }
76749
- if (!assumeSucceeded) {
76750
- forwardAwsEnv(dockerEnv);
76751
- applyProfileCredentialsOverlay(dockerEnv, profileCredentials, false);
76752
- if (profileCredsFile) {
76753
- dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = profileCredsFile.containerPath;
76754
- dockerEnv["AWS_PROFILE"] = profileCredsFile.profileName;
76755
- }
76756
- }
76967
+ await applyLambdaCredentialEnv(dockerEnv, {
76968
+ ...resolvedAssumeRoleArn && { assumeRoleArn: resolvedAssumeRoleArn },
76969
+ ...options.region !== void 0 && { region: options.region },
76970
+ ...profileCredentials && { profileCredentials },
76971
+ ...profileCredsFile && { profileCredsFile }
76972
+ });
76757
76973
  let debugPort;
76758
76974
  if (options.debugPort) {
76759
76975
  debugPort = Number(options.debugPort);
@@ -77153,6 +77369,55 @@ async function readStdin() {
77153
77369
  return Buffer.concat(chunks).toString("utf-8");
77154
77370
  }
77155
77371
  /**
77372
+ * Inject AWS credentials into the container env. Precedence:
77373
+ * 1. `--assume-role` → STS-issued temp creds for the resolved ARN (on
77374
+ * STS failure, warn + fall through to dev creds). Swapping the
77375
+ * developer's credentials for the function's deployed execution role
77376
+ * degrades rather than hard-errors: this is a developer-loop tool, not
77377
+ * a security boundary, and the most common cause is the role's
77378
+ * `AssumeRolePolicy` not trusting the developer's IAM principal (a
77379
+ * config gap, not a cdkd bug).
77380
+ * 2. dev shell creds (`forwardAwsEnv` — which folds the region case for the
77381
+ * same reason arm 1 does) + `--profile` overlay
77382
+ * ({@link applyProfileCredentialsOverlay}) + the bind-mounted
77383
+ * credentials-file env so handler `fromIni({ profile })` resolves.
77384
+ * Applied in that order: the overlay intentionally wins over whatever
77385
+ * `forwardAwsEnv` copied, and the credentials-file env is additive.
77386
+ *
77387
+ * Extracted from `localInvokeCommand`'s body by issue
77388
+ * [#1836](https://github.com/go-to-k/cdkd/issues/1836) and shaped to mirror
77389
+ * `applyAgentCoreCredentialEnv` in `local-invoke-agentcore.ts` (the sibling
77390
+ * issue [#1814](https://github.com/go-to-k/cdkd/issues/1814) fixed), so a unit
77391
+ * test can lock the STS-client region binding with a mocked STS instead of
77392
+ * driving the full synth + docker pipeline.
77393
+ */
77394
+ async function applyLambdaCredentialEnv(dockerEnv, args) {
77395
+ const logger = getLogger();
77396
+ let assumeSucceeded = false;
77397
+ if (args.assumeRoleArn) {
77398
+ const stsRegion = canonicalizeRegion(args.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"]);
77399
+ try {
77400
+ const creds = await assumeLambdaExecutionRole(args.assumeRoleArn, stsRegion);
77401
+ dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
77402
+ dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
77403
+ dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
77404
+ if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
77405
+ assumeSucceeded = true;
77406
+ } catch (err) {
77407
+ const reason = err instanceof Error ? err.message : String(err);
77408
+ logger.warn(`--assume-role: STS AssumeRole(${args.assumeRoleArn}) failed: ${reason}. Falling back to the developer's shell credentials.`);
77409
+ }
77410
+ }
77411
+ if (!assumeSucceeded) {
77412
+ forwardAwsEnv(dockerEnv);
77413
+ applyProfileCredentialsOverlay(dockerEnv, args.profileCredentials, false);
77414
+ if (args.profileCredsFile) {
77415
+ dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = args.profileCredsFile.containerPath;
77416
+ dockerEnv["AWS_PROFILE"] = args.profileCredsFile.profileName;
77417
+ }
77418
+ }
77419
+ }
77420
+ /**
77156
77421
  * Assume the Lambda execution role and return temporary credentials.
77157
77422
  *
77158
77423
  * Closes the "developer has admin creds, the deployed function has narrow
@@ -77192,17 +77457,30 @@ async function assumeLambdaExecutionRole(roleArn, region) {
77192
77457
  *
77193
77458
  * Region is inherited from `AWS_REGION` / `AWS_DEFAULT_REGION` so
77194
77459
  * `aws.config.region` inside the handler works without extra setup.
77460
+ *
77461
+ * Issue [#1836](https://github.com/go-to-k/cdkd/issues/1836): the two REGION
77462
+ * entries are folded through `canonicalizeRegion` on the way in, the credentials
77463
+ * are copied verbatim. This is the DEFAULT path — no `--assume-role` — and it
77464
+ * carries the same exposure the assume-role branch had: the value lands in the
77465
+ * container's own `AWS_REGION`, i.e. in every SDK client the handler builds, and
77466
+ * SDK endpoint resolution is case-SENSITIVE (`CN-NORTH-1` resolves the
77467
+ * COMMERCIAL partition). Folding only inside `applyLambdaCredentialEnv`'s
77468
+ * assume-role arm made ONE command yield two different container regions for the
77469
+ * same shell — `cn-north-1` with `--assume-role`, `CN-NORTH-1` without.
77195
77470
  */
77196
77471
  function forwardAwsEnv(env) {
77197
- for (const key of [
77472
+ const passThrough = [
77198
77473
  "AWS_ACCESS_KEY_ID",
77199
77474
  "AWS_SECRET_ACCESS_KEY",
77200
77475
  "AWS_SESSION_TOKEN",
77201
77476
  "AWS_REGION",
77202
77477
  "AWS_DEFAULT_REGION"
77203
- ]) {
77478
+ ];
77479
+ const regionKeys = /* @__PURE__ */ new Set(["AWS_REGION", "AWS_DEFAULT_REGION"]);
77480
+ for (const key of passThrough) {
77204
77481
  const value = process.env[key];
77205
- if (value !== void 0) env[key] = value;
77482
+ if (value === void 0) continue;
77483
+ env[key] = regionKeys.has(key) ? canonicalizeRegion(value) : value;
77206
77484
  }
77207
77485
  }
77208
77486
  /**
@@ -78448,7 +78726,7 @@ function createMigrateCommand() {
78448
78726
  */
78449
78727
  function buildProgram() {
78450
78728
  const program = new Command();
78451
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.13");
78729
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.15");
78452
78730
  program.hook("preAction", (_thisCommand, actionCommand) => {
78453
78731
  const { profile } = actionCommand.optsWithGlobals();
78454
78732
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;