@go-to-k/cdkd 0.283.12 → 0.283.14

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-DHOpGcMS.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-B8gQpv93.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-BKR0cF_X.js";
4
+ import { t as ASGProvider } from "./asg-provider-CgnPBnOS.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";
@@ -20534,6 +20534,128 @@ var SSMParameterProvider = class {
20534
20534
  return out;
20535
20535
  }
20536
20536
  /**
20537
+ * Build the `Arn` attribute for an SSM parameter (issue #1824).
20538
+ *
20539
+ * WHY THIS EXISTS. An output / cross-resource `Fn::GetAtt` reads the CACHED
20540
+ * `resource.attributes[<CFnName>]` in
20541
+ * `IntrinsicFunctionResolver.constructAttribute`, which never calls a
20542
+ * provider's `getAttribute` — and for an `*Arn` name the resolver's shape
20543
+ * guard HARD-FAILS rather than degrading when the cached value is missing and
20544
+ * the physicalId is name-shaped (an SSM physicalId IS the parameter name). So
20545
+ * `new cdk.CfnOutput(this, 'ParamArn', { value: param.attrArn })` failed the
20546
+ * deploy until the ARN was cached under its CFn name.
20547
+ *
20548
+ * SOURCE DECISION — CONSTRUCTED, not read back. `PutParameter` returns only
20549
+ * `Version` / `Tier`, so there were exactly two candidate sources, and the
20550
+ * rejected one is recorded here so the call stays challengeable:
20551
+ *
20552
+ * - `GetParameter`'s `Parameter.ARN` is authoritative, and cdkd already
20553
+ * issues that call in `readCurrentState` / `import`, so it needs no new IAM
20554
+ * permission. It was REJECTED on COST: it would be a brand-new round trip
20555
+ * on EVERY parameter create AND every update, and SSM parameters are among
20556
+ * the most numerous resources in real CDK stacks — a 50-parameter stack
20557
+ * would pay 50 extra reads for a value that is a pure function of data
20558
+ * cdkd already holds, against a project whose whole premise is removing
20559
+ * round trips.
20560
+ * - CONSTRUCTION is deterministic. The parameter ARN is the documented
20561
+ * `arn:{partition}:ssm:{region}:{account}:parameter/{name}`, with the
20562
+ * name's leading `/` folded into the separator — the same formula
20563
+ * `aws-cdk-lib`'s `StringParameter.parameterArn` uses — so nothing has to
20564
+ * be discovered from AWS. It is also the established idiom in this repo for
20565
+ * "the create response carries no ARN": `AppSyncProvider.buildAppSyncArn`
20566
+ * and the four `CloudControlProvider` enrichment sites (KMS key, ECR
20567
+ * repository x2, Kinesis stream) all construct through the same
20568
+ * `getAccountInfo` + `derivePartitionAndUrlSuffix` + `fabricated`-refusal
20569
+ * pair this method reuses.
20570
+ *
20571
+ * The PARTITION is DERIVED, never hardcoded `arn:aws:` — that hardcoding is an
20572
+ * active bug class here (#1794 / #1815), and on a `cn-` / `us-gov-` deploy it
20573
+ * would record an ARN naming a partition the parameter is not in.
20574
+ *
20575
+ * The `fabricated`-account guard (#1730 / #1746) is honored by REFUSING rather
20576
+ * than by substituting: `getAccountInfo` catches its own STS failure and
20577
+ * answers the placeholder `123456789012`, which carries no wildcard and is
20578
+ * therefore invisible to `isPlaceholderArn`, so a fabricated id must yield NO
20579
+ * attribute instead of a plausible-looking wrong one. Returning `undefined`
20580
+ * degrades to exactly the pre-fix behavior — the resolver's shape guard fails
20581
+ * LOUDLY on the name-shaped physicalId — never to a silently wrong value.
20582
+ *
20583
+ * DEGRADATION ON THE UPDATE PATH is a DELIBERATE trade-off, not one this
20584
+ * design avoids. An update result's `attributes` REPLACE the state record's
20585
+ * rather than merging into it (`deploy-engine.ts`:
20586
+ * `result.attributes ?? (wasReplaced ? undefined : currentResource.attributes)`),
20587
+ * so whenever this method degrades during an `update` the returned map carries
20588
+ * no `Arn` and the create-time one is DROPPED. Construction does not prevent
20589
+ * that — it only swaps WHICH dependency can fail, STS plus the client's region
20590
+ * resolver instead of SSM — so read the source decision above as buying one
20591
+ * fewer round trip and nothing else.
20592
+ *
20593
+ * The alternative is to return NO `attributes` at all on the degraded path, so
20594
+ * the engine's `??` carries the previous map forward and the `Arn` survives.
20595
+ * That is REJECTED: the same map carries `Type` / `Value`, which are the values
20596
+ * THIS update just sent. Carrying the previous map forward would answer
20597
+ * `Fn::GetAtt [Param, Value]` with the SUPERSEDED value — silently, and to a
20598
+ * question cdkd can answer correctly — and would hand `cdkd drift` a baseline
20599
+ * AWS does not hold. Dropping the `Arn` instead degrades to the resolver's LOUD
20600
+ * `*Arn` shape-guard failure, which is exactly the pre-fix behavior, and the
20601
+ * resource's next UPDATE re-records it. A loud missing value beats a quiet
20602
+ * wrong one — the same reasoning the `fabricated`-account arm above applies.
20603
+ *
20604
+ * Two honest bounds on that "next update re-records it". `getAccountInfo`
20605
+ * caches a SUCCESSFUL identity for the process, so within one deploy only a run
20606
+ * whose FIRST `GetCallerIdentity` fails degrades at all — but the 10s
20607
+ * fabricated-answer TTL then covers every parameter created in that window
20608
+ * rather than a random subset. And a plain re-deploy does NOT heal it: a
20609
+ * resource whose resolved properties equal its state record is skipped with no
20610
+ * provider call, so healing needs a real property change (issue
20611
+ * [#1852](https://github.com/go-to-k/cdkd/issues/1852), which also covers the
20612
+ * resolver message that currently misattributes this to a missing enrichment).
20613
+ *
20614
+ * NEVER THROWS, and that is enforced here rather than asserted at the call
20615
+ * sites. `create` calls this AFTER `PutParameter` has already committed a
20616
+ * parameter, so a throw would surface as a failed create over a missing
20617
+ * ATTRIBUTE — leaving an orphan that makes the next deploy hit
20618
+ * `ParameterAlreadyExists` (the issue #376 class), and the cleanup that exists
20619
+ * for that is deliberately out of reach by then. `getAccountInfo` catches its
20620
+ * own STS failure, but `config.region()` is a resolver that can reject, so the
20621
+ * whole body is wrapped: an unexpected failure degrades to "no Arn recorded"
20622
+ * exactly as the fabricated-account arm does.
20623
+ */
20624
+ async buildParameterArn(name) {
20625
+ try {
20626
+ return await this.buildParameterArnUnguarded(name);
20627
+ } catch (error) {
20628
+ this.logger.warn(`Could not build the Arn attribute for SSM parameter ${name}: ${error instanceof Error ? error.message : String(error)}. The parameter itself is unaffected; the Arn is NOT recorded, so an Fn::GetAtt on it will fail until a later deploy records it.`);
20629
+ return;
20630
+ }
20631
+ }
20632
+ /**
20633
+ * {@link buildParameterArn}'s body. Only that method may call it.
20634
+ *
20635
+ * `name` is always a parameter NAME, never an ARN, and that is an INVARIANT
20636
+ * rather than an assumption this method has to defend (issue #1824 review
20637
+ * round 3). Both callers pass a value SSM has just accepted as a
20638
+ * `PutParameter` `Name`, and that field forbids an ARN outright — "You can't
20639
+ * enter the Amazon Resource Name (ARN) for a parameter, only the parameter
20640
+ * name itself", plus a name charset of `a-zA-Z0-9_.-` and `/` that excludes
20641
+ * `:`. So an ARN-shaped id fails at `PutParameter` several statements ABOVE
20642
+ * this method on both paths, and a guard here could only ever fire against a
20643
+ * mock. Round 2 added exactly such a guard, believing an ARN could arrive from
20644
+ * `cdkd import --resource Param=arn:...`; it can, but the defect is that the
20645
+ * import RECORDED it, so the fix belongs at the import boundary — where
20646
+ * `import()` now refuses it — and not here. Do not re-add it.
20647
+ */
20648
+ async buildParameterArnUnguarded(name) {
20649
+ const accountInfo = await getAccountInfo(await this.ssmClient.config.region());
20650
+ if (accountInfo.fabricated) {
20651
+ this.logger.warn(`Cannot determine the AWS account (STS is unreachable, and the resolved account id is a placeholder), so the Arn attribute for SSM parameter ${name} would be fabricated and is NOT recorded. An Fn::GetAtt on it will fail until a later deploy resolves the account.`);
20652
+ return;
20653
+ }
20654
+ const { partition } = derivePartitionAndUrlSuffix(accountInfo.region);
20655
+ const qualifiedName = name.startsWith("/") ? name.slice(1) : name;
20656
+ return `arn:${partition}:ssm:${canonicalizeRegion(accountInfo.region)}:${accountInfo.accountId}:parameter/${qualifiedName}`;
20657
+ }
20658
+ /**
20537
20659
  * Create an SSM parameter
20538
20660
  */
20539
20661
  async create(logicalId, resourceType, properties) {
@@ -20575,11 +20697,13 @@ var SSMParameterProvider = class {
20575
20697
  throw innerError;
20576
20698
  }
20577
20699
  this.logger.debug(`Successfully created SSM parameter ${logicalId}: ${name}`);
20700
+ const arn = await this.buildParameterArn(name);
20578
20701
  return {
20579
20702
  physicalId: name,
20580
20703
  attributes: {
20581
20704
  Type: type,
20582
- Value: value
20705
+ Value: value,
20706
+ ...arn !== void 0 && { Arn: arn }
20583
20707
  }
20584
20708
  };
20585
20709
  } catch (error) {
@@ -20626,12 +20750,14 @@ var SSMParameterProvider = class {
20626
20750
  this.logger.debug(`Updated tags for SSM parameter ${physicalId}`);
20627
20751
  }
20628
20752
  this.logger.debug(`Successfully updated SSM parameter ${logicalId}`);
20753
+ const arn = await this.buildParameterArn(physicalId);
20629
20754
  return {
20630
20755
  physicalId,
20631
20756
  wasReplaced: false,
20632
20757
  attributes: {
20633
20758
  Type: type,
20634
- Value: value
20759
+ Value: value,
20760
+ ...arn !== void 0 && { Arn: arn }
20635
20761
  }
20636
20762
  };
20637
20763
  } catch (error) {
@@ -20742,6 +20868,57 @@ var SSMParameterProvider = class {
20742
20868
  return result;
20743
20869
  }
20744
20870
  /**
20871
+ * Refuse a physical id SSM's WRITE APIs could never accept (issue #1824
20872
+ * review round 3).
20873
+ *
20874
+ * An SSM parameter's physicalId must always be the parameter NAME, because
20875
+ * that is the only thing the write APIs take. `PutParameterRequest.Name`:
20876
+ * "You can't enter the Amazon Resource Name (ARN) for a parameter, only the
20877
+ * parameter name itself", with a name charset of `a-zA-Z0-9_.-` plus `/` as
20878
+ * the hierarchy separator; `DeleteParameterRequest.Name` repeats the ARN
20879
+ * prohibition verbatim. A COLON therefore cannot appear in any writable name,
20880
+ * which is the single predicate used here — it covers both shapes
20881
+ * `GetParameter` additionally accepts and this method has to stop:
20882
+ *
20883
+ * - an ARN (`arn:aws:ssm:us-east-1:111122223333:parameter/foo`), and
20884
+ * - a version / label SELECTOR (`GetParameterRequest.Name`: "To query by
20885
+ * parameter label, use `"Name": "name:label"`. To query by parameter
20886
+ * version, use `"Name": "name:version"`").
20887
+ *
20888
+ * Both sail through this provider's existence check, so before this guard the
20889
+ * value was RECORDED as the physicalId and the damage landed later and
20890
+ * elsewhere: `cdkd deploy` failing at `PutParameter({Name: physicalId})` with
20891
+ * a `ValidationException` after a `Value` change, and `cdkd destroy` failing
20892
+ * identically at `DeleteParameter`. This is where the bad value ENTERS
20893
+ * (`--resource` / `Properties.Name`), so it is where it is stopped.
20894
+ *
20895
+ * REFUSED, NOT NORMALIZED to the name, and the ARN grammar is the reason. The
20896
+ * mapping back is genuinely ambiguous rather than merely awkward:
20897
+ *
20898
+ * - The leading `/` is NOT recoverable. `arn:…:parameter/foo` is the ARN of
20899
+ * BOTH the simple name `foo` and the one-level hierarchical name `/foo`,
20900
+ * since the name's leading slash IS the separator — `aws-cdk-lib`'s
20901
+ * `arnForParameterName` renders the two to the identical string, and that
20902
+ * is precisely why CDK needs an explicit `simpleName` flag whenever the
20903
+ * name is a token. Guessing wrong writes a physicalId naming a DIFFERENT
20904
+ * parameter, i.e. converts a loud failure into a silent wrong-resource
20905
+ * write.
20906
+ * - A parameter SHARED from another account has no name form at all
20907
+ * ("For parameters shared with you from another account, you must use the
20908
+ * full ARN"), so for that shape there is nothing to normalize TO.
20909
+ *
20910
+ * A refusal is a loud, fixable error, and the message names the exact command
20911
+ * that prints the name to pass instead.
20912
+ */
20913
+ refuseUnwritableParameterId(input, explicit) {
20914
+ if (!explicit.includes(":")) return;
20915
+ const isArn = explicit.startsWith("arn:");
20916
+ const shape = isArn ? "an ARN" : "a version / label selector";
20917
+ const remedy = input.knownPhysicalId === explicit ? `pass the parameter NAME instead: --resource ${input.logicalId}=<parameterName>` : `set Properties.Name to the parameter NAME rather than ${shape}`;
20918
+ const whyNotDerived = isArn ? " cdkd deliberately does NOT derive the name from the ARN: the leading \"/\" is not recoverable (arn:...:parameter/foo is the ARN of BOTH the simple name \"foo\" and the hierarchical name \"/foo\"), and a parameter shared from another account has no name form at all — so a derived name could silently address a DIFFERENT parameter." : "";
20919
+ throw new ProvisioningError(`Cannot adopt SSM parameter ${input.logicalId} from ${shape} ('${explicit}'): cdkd records a parameter's NAME as its physical id, because SSM's write APIs accept only the name (PutParameter and DeleteParameter both reject an ARN, and a name cannot contain ':'), so the next cdkd deploy and cdkd destroy would fail with a ValidationException. ${remedy}.${whyNotDerived} Read the name AWS holds with: aws ssm get-parameter --name '${explicit}' --query Parameter.Name --output text`, input.resourceType, input.logicalId);
20920
+ }
20921
+ /**
20745
20922
  * Adopt an existing SSM parameter into cdkd state.
20746
20923
  *
20747
20924
  * SSM physical IDs ARE the parameter names (`/foo/bar`). The CDK template
@@ -20749,19 +20926,25 @@ var SSMParameterProvider = class {
20749
20926
  * covers most cases.
20750
20927
  *
20751
20928
  * Lookup order:
20752
- * 1. `--resource` override or `Properties.Name` → verify via `GetParameter`.
20929
+ * 1. `--resource` override or `Properties.Name` → refuse it outright when it
20930
+ * is not a writable NAME (see {@link refuseUnwritableParameterId} — note
20931
+ * `GetParameter` accepts an ARN and a `name:version` selector while every
20932
+ * write API rejects both), else verify via `GetParameter`.
20753
20933
  */
20754
20934
  async import(input) {
20755
20935
  const explicit = resolveExplicitPhysicalId(input, "Name");
20756
- if (explicit) try {
20757
- await this.ssmClient.send(new GetParameterCommand({ Name: explicit }));
20758
- return {
20759
- physicalId: explicit,
20760
- attributes: {}
20761
- };
20762
- } catch (err) {
20763
- if (err instanceof ParameterNotFound) return null;
20764
- throw err;
20936
+ if (explicit) {
20937
+ this.refuseUnwritableParameterId(input, explicit);
20938
+ try {
20939
+ const arn = (await this.ssmClient.send(new GetParameterCommand({ Name: explicit }))).Parameter?.ARN;
20940
+ return {
20941
+ physicalId: explicit,
20942
+ attributes: { ...arn !== void 0 && { Arn: arn } }
20943
+ };
20944
+ } catch (err) {
20945
+ if (err instanceof ParameterNotFound) return null;
20946
+ throw err;
20947
+ }
20765
20948
  }
20766
20949
  return null;
20767
20950
  }
@@ -33740,16 +33923,20 @@ var RDSProvider = class {
33740
33923
  });
33741
33924
  try {
33742
33925
  const tags = this.buildTags(properties);
33743
- await this.getClient().send(new CreateDBSubnetGroupCommand({
33926
+ const created = await this.getClient().send(new CreateDBSubnetGroupCommand({
33744
33927
  DBSubnetGroupName: dbSubnetGroupName,
33745
33928
  DBSubnetGroupDescription: properties["DBSubnetGroupDescription"] || `Subnet group for ${logicalId}`,
33746
33929
  SubnetIds: properties["SubnetIds"],
33747
33930
  ...tags.length > 0 && { Tags: tags }
33748
33931
  }));
33749
33932
  this.logger.debug(`Successfully created DBSubnetGroup ${logicalId}: ${dbSubnetGroupName}`);
33933
+ const dbSubnetGroupArn = created.DBSubnetGroup?.DBSubnetGroupArn;
33750
33934
  return {
33751
33935
  physicalId: dbSubnetGroupName,
33752
- attributes: { DBSubnetGroupName: dbSubnetGroupName }
33936
+ attributes: {
33937
+ DBSubnetGroupName: dbSubnetGroupName,
33938
+ ...dbSubnetGroupArn !== void 0 && { DBSubnetGroupArn: dbSubnetGroupArn }
33939
+ }
33753
33940
  };
33754
33941
  } catch (error) {
33755
33942
  const cause = error instanceof Error ? error : void 0;
@@ -33773,7 +33960,10 @@ var RDSProvider = class {
33773
33960
  return {
33774
33961
  physicalId,
33775
33962
  wasReplaced: false,
33776
- attributes: { DBSubnetGroupName: physicalId }
33963
+ attributes: {
33964
+ DBSubnetGroupName: physicalId,
33965
+ ...arn !== void 0 && { DBSubnetGroupArn: arn }
33966
+ }
33777
33967
  };
33778
33968
  } catch (error) {
33779
33969
  const cause = error instanceof Error ? error : void 0;
@@ -34379,10 +34569,10 @@ var RDSProvider = class {
34379
34569
  async importDBSubnetGroup(input) {
34380
34570
  const explicit = resolveExplicitPhysicalId(input, "DBSubnetGroupName");
34381
34571
  if (explicit) try {
34382
- await this.getClient().send(new DescribeDBSubnetGroupsCommand({ DBSubnetGroupName: explicit }));
34572
+ const arn = (await this.getClient().send(new DescribeDBSubnetGroupsCommand({ DBSubnetGroupName: explicit }))).DBSubnetGroups?.[0]?.DBSubnetGroupArn;
34383
34573
  return {
34384
34574
  physicalId: explicit,
34385
- attributes: {}
34575
+ attributes: { ...arn !== void 0 && { DBSubnetGroupArn: arn } }
34386
34576
  };
34387
34577
  } catch (err) {
34388
34578
  if (err.name === "DBSubnetGroupNotFoundFault") return null;
@@ -68668,12 +68858,21 @@ function indexGrandchildTemplatePaths(childTemplate, childTemplatePath) {
68668
68858
  * `--stack-region`, bucket resolution failure) logs at warn and returns
68669
68859
  * `undefined`. Auth failures and other genuine errors propagate.
68670
68860
  *
68671
- * Read-only against state — no lock acquisition or save path here.
68861
+ * No lock acquisition and no `state.json` write path here. That is NOT the
68862
+ * same as "read-only against the bucket", and the difference was mis-stated
68863
+ * until issue #1836: {@link buildCrossStackResolver}'s exports-index lookup
68864
+ * goes through {@link ExportIndexStore}, whose `load` REBUILDS the index from
68865
+ * the per-stack state files on a miss and PUTs the rebuilt object
68866
+ * (`cdkd/_index/{region}/exports.json`). So an `Fn::ImportValue` lookup from a
68867
+ * `cdkd local` command can write that one derived key. Keeping the index KEY
68868
+ * the same spelling the state records carry is what keeps that write CORRECT
68869
+ * rather than an empty index — see the note on `consumerRegion` below.
68672
68870
  */
68673
68871
  async function loadStateForStack(stackName, synthRegion, opts) {
68674
68872
  const logger = getLogger();
68675
68873
  const prefix = opts.logPrefix ?? "--from-state";
68676
- const region = opts.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? synthRegion ?? "us-east-1";
68874
+ const region = canonicalizeRegion(opts.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? synthRegion ?? "us-east-1");
68875
+ const clientRegion = canonicalizeRegion(opts.region) || void 0;
68677
68876
  let stateBucket;
68678
68877
  try {
68679
68878
  stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, region);
@@ -68682,7 +68881,7 @@ async function loadStateForStack(stackName, synthRegion, opts) {
68682
68881
  return;
68683
68882
  }
68684
68883
  const awsClients = new AwsClients({
68685
- ...opts.region !== void 0 && { region: opts.region },
68884
+ ...clientRegion !== void 0 && { region: clientRegion },
68686
68885
  ...opts.profile !== void 0 && { profile: opts.profile }
68687
68886
  });
68688
68887
  setAwsClients(awsClients);
@@ -68692,7 +68891,7 @@ async function loadStateForStack(stackName, synthRegion, opts) {
68692
68891
  prefix: opts.statePrefix
68693
68892
  };
68694
68893
  const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
68695
- ...opts.region !== void 0 && { region: opts.region },
68894
+ ...clientRegion !== void 0 && { region: clientRegion },
68696
68895
  ...opts.profile !== void 0 && { profile: opts.profile }
68697
68896
  });
68698
68897
  await stateBackend.verifyBucketExists();
@@ -68701,15 +68900,58 @@ async function loadStateForStack(stackName, synthRegion, opts) {
68701
68900
  logger.warn(`${prefix}: no cdkd state found for stack '${stackName}' in bucket '${stateBucket}'. Was it deployed via 'cdkd deploy'? Falling back.`);
68702
68901
  return;
68703
68902
  }
68903
+ /**
68904
+ * Issue #1836: EXACT match first, case-insensitive second.
68905
+ *
68906
+ * `S3StateBackend.listStacks` dedupes on the EXACT `{stack}\0{region}` pair,
68907
+ * so `cdkd/MyStack/US-EAST-1/state.json` and
68908
+ * `cdkd/MyStack/us-east-1/state.json` are two DISTINCT refs — and
68909
+ * ListObjectsV2 returns them in ASCII order, i.e. the upper-cased one FIRST.
68910
+ * A fold-only `find` therefore hands `--stack-region us-east-1` the OTHER
68911
+ * record, silently reading state the user did not name; the pre-fold `===`
68912
+ * got that case right. So the fold is the RECOVERY for a case mismatch, never
68913
+ * an override of a spelling that exists verbatim.
68914
+ *
68915
+ * `candidate` is therefore the spelling the USER supplied, not the folded
68916
+ * one (round 3 of the #1836 review): the fold at each command's handler
68917
+ * entry and at the `--from-state` factory is what an SDK client needs, but
68918
+ * feeding it to this compare made the exact-match rule unreachable from any
68919
+ * real CLI invocation — every candidate was already canonical, so `exact`
68920
+ * could only ever match an already-canonical record and `--stack-region
68921
+ * US-EAST-1` read the `us-east-1` one while REPORTING it as the exact
68922
+ * spelling. `opts.rawStackRegion` carries the unfolded value here; the
68923
+ * recovery arm folds both sides itself, so nothing upstream has to.
68924
+ *
68925
+ * When more than one ref is canonical-equal the choice is announced, because
68926
+ * either answer reads some record the user did not spell out in full and the
68927
+ * silence is what made the original miss hard to diagnose. The wording
68928
+ * states which of the two rules decided it — and does so from the same
68929
+ * `exact` binding the choice was made from, so it cannot claim an exact
68930
+ * match the compare did not make.
68931
+ */
68932
+ const findByRegion = (candidate, source) => {
68933
+ const exact = refs.find((r) => r.region === candidate);
68934
+ const folded = refs.filter((r) => canonicalizeRegion(r.region) === canonicalizeRegion(candidate));
68935
+ const chosen = exact ?? folded[0];
68936
+ if (folded.length > 1 && chosen) {
68937
+ const how = exact ? `matches ${source} '${candidate}' exactly` : `no record spells ${source} '${candidate}' exactly, so this is a case-insensitive recovery`;
68938
+ 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}.`);
68939
+ }
68940
+ return chosen;
68941
+ };
68942
+ let synthMatch;
68943
+ if (!opts.stackRegion && synthRegion) synthMatch = findByRegion(synthRegion, "the synth-derived stack region");
68704
68944
  let targetRegion;
68705
68945
  if (opts.stackRegion) {
68706
- if (!refs.find((r) => r.region === opts.stackRegion)) {
68946
+ const requestedRegion = opts.rawStackRegion ?? opts.stackRegion;
68947
+ const found = findByRegion(requestedRegion, "--stack-region");
68948
+ if (!found) {
68707
68949
  const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
68708
- logger.warn(`${prefix}: stack '${stackName}' has no state in region '${opts.stackRegion}' (available: ${seen}). Falling back.`);
68950
+ logger.warn(`${prefix}: stack '${stackName}' has no state in region '${requestedRegion}' (available: ${seen}). Falling back.`);
68709
68951
  return;
68710
68952
  }
68711
- targetRegion = opts.stackRegion;
68712
- } else if (synthRegion && refs.some((r) => r.region === synthRegion)) targetRegion = synthRegion;
68953
+ targetRegion = found.region ?? requestedRegion;
68954
+ } else if (synthRegion && synthMatch) targetRegion = synthMatch.region ?? synthRegion;
68713
68955
  else if (refs.length === 1) targetRegion = refs[0].region ?? synthRegion ?? region;
68714
68956
  else {
68715
68957
  const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
@@ -68758,7 +69000,8 @@ async function loadStateForStack(stackName, synthRegion, opts) {
68758
69000
  async function loadBootstrapContainerRepo(synthRegion, opts) {
68759
69001
  const logger = getLogger();
68760
69002
  const prefix = opts.logPrefix ?? "--from-state";
68761
- const region = opts.region ?? opts.stackRegion ?? synthRegion ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? "us-east-1";
69003
+ const rawRegion = opts.region ?? opts.rawStackRegion ?? opts.stackRegion ?? synthRegion ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? "us-east-1";
69004
+ const region = canonicalizeRegion(rawRegion);
68762
69005
  let stateBucket;
68763
69006
  try {
68764
69007
  stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, region);
@@ -68766,8 +69009,9 @@ async function loadBootstrapContainerRepo(synthRegion, opts) {
68766
69009
  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.`);
68767
69010
  return;
68768
69011
  }
69012
+ const clientRegion = canonicalizeRegion(opts.region) || void 0;
68769
69013
  const awsClients = new AwsClients({
68770
- ...opts.region !== void 0 && { region: opts.region },
69014
+ ...clientRegion !== void 0 && { region: clientRegion },
68771
69015
  ...opts.profile !== void 0 && { profile: opts.profile }
68772
69016
  });
68773
69017
  setAwsClients(awsClients);
@@ -68777,16 +69021,25 @@ async function loadBootstrapContainerRepo(synthRegion, opts) {
68777
69021
  prefix: opts.statePrefix
68778
69022
  };
68779
69023
  const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
68780
- ...opts.region !== void 0 && { region: opts.region },
69024
+ ...clientRegion !== void 0 && { region: clientRegion },
68781
69025
  ...opts.profile !== void 0 && { profile: opts.profile }
68782
69026
  });
68783
69027
  const markerKey = getBootstrapMarkerKey(region);
68784
- const body = await stateBackend.getRawObject(markerKey);
69028
+ let resolvedKey = markerKey;
69029
+ let body = await stateBackend.getRawObject(markerKey);
69030
+ if (body === null && rawRegion !== region) {
69031
+ const rawKey = getBootstrapMarkerKey(rawRegion);
69032
+ body = await stateBackend.getRawObject(rawKey);
69033
+ if (body !== null) {
69034
+ resolvedKey = rawKey;
69035
+ logger.debug(`${prefix}: bootstrap marker found at the un-folded key '${rawKey}' (no '${markerKey}') — an upper-cased region was used at 'cdkd bootstrap' time.`);
69036
+ }
69037
+ }
68785
69038
  if (body === null) {
68786
- logger.debug(`${prefix}: no bootstrap marker at '${markerKey}' in bucket '${stateBucket}' — assuming conventional asset-repo names.`);
69039
+ logger.debug(`${prefix}: no bootstrap marker at '${markerKey}'${rawRegion !== region ? ` (nor '${getBootstrapMarkerKey(rawRegion)}')` : ""} in bucket '${stateBucket}' — assuming conventional asset-repo names.`);
68787
69040
  return;
68788
69041
  }
68789
- const marker = parseBootstrapMarker(body, markerKey);
69042
+ const marker = parseBootstrapMarker(body, resolvedKey);
68790
69043
  logger.debug(`${prefix}: bootstrap marker for ${region} names container repo '${marker.containerRepo}'.`);
68791
69044
  return marker.containerRepo;
68792
69045
  } catch (err) {
@@ -68825,13 +69078,14 @@ async function buildCrossStackResolver(consumerRegion, opts) {
68825
69078
  const prefix = opts.logPrefix ?? "--from-state";
68826
69079
  let stateBucket;
68827
69080
  try {
68828
- stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, consumerRegion);
69081
+ stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, canonicalizeRegion(consumerRegion));
68829
69082
  } catch (err) {
68830
69083
  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.`);
68831
69084
  return;
68832
69085
  }
69086
+ const clientRegion = canonicalizeRegion(opts.region) || void 0;
68833
69087
  const awsClients = new AwsClients({
68834
- ...opts.region !== void 0 && { region: opts.region },
69088
+ ...clientRegion !== void 0 && { region: clientRegion },
68835
69089
  ...opts.profile !== void 0 && { profile: opts.profile }
68836
69090
  });
68837
69091
  const stateConfig = {
@@ -68839,7 +69093,7 @@ async function buildCrossStackResolver(consumerRegion, opts) {
68839
69093
  prefix: opts.statePrefix
68840
69094
  };
68841
69095
  const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
68842
- ...opts.region !== void 0 && { region: opts.region },
69096
+ ...clientRegion !== void 0 && { region: clientRegion },
68843
69097
  ...opts.profile !== void 0 && { profile: opts.profile }
68844
69098
  });
68845
69099
  try {
@@ -68871,9 +69125,10 @@ async function buildCrossStackResolver(consumerRegion, opts) {
68871
69125
  logger.debug(`${prefix}: failed to list stacks during Fn::ImportValue fallback for '${exportName}': ${err instanceof Error ? err.message : String(err)}`);
68872
69126
  return;
68873
69127
  }
69128
+ const canonicalConsumerRegion = canonicalizeRegion(consumerRegion);
68874
69129
  for (const ref of refs) {
68875
69130
  const region = ref.region ?? consumerRegion;
68876
- if (region !== consumerRegion) continue;
69131
+ if (canonicalizeRegion(region) !== canonicalConsumerRegion) continue;
68877
69132
  try {
68878
69133
  const got = await stateBackend.getState(ref.stackName, region);
68879
69134
  if (!got || !got.state.outputs) continue;
@@ -68890,14 +69145,31 @@ async function buildCrossStackResolver(consumerRegion, opts) {
68890
69145
  }
68891
69146
  },
68892
69147
  async resolveGetStackOutput(producerStack, producerRegion, outputName) {
68893
- try {
68894
- const got = await stateBackend.getState(producerStack, producerRegion);
69148
+ const readOutput = (got) => {
68895
69149
  if (!got || !got.state.outputs) return void 0;
68896
69150
  if (!(outputName in got.state.outputs)) return void 0;
68897
69151
  const value = got.state.outputs[outputName];
68898
69152
  if (typeof value === "string") return value;
68899
69153
  if (typeof value === "number" || typeof value === "boolean") return String(value);
68900
69154
  return JSON.stringify(value);
69155
+ };
69156
+ try {
69157
+ const got = await stateBackend.getState(producerStack, producerRegion);
69158
+ if (got) return readOutput(got);
69159
+ const canonicalProducerRegion = canonicalizeRegion(producerRegion);
69160
+ const refs = await stateBackend.listStacks();
69161
+ for (const ref of refs) {
69162
+ if (ref.stackName !== producerStack) continue;
69163
+ const region = ref.region ?? producerRegion;
69164
+ if (region === producerRegion) continue;
69165
+ if (canonicalizeRegion(region) !== canonicalProducerRegion) continue;
69166
+ const recovered = readOutput(await stateBackend.getState(producerStack, region));
69167
+ if (recovered !== void 0) {
69168
+ logger.debug(`${prefix}: Fn::GetStackOutput '${producerStack}.${outputName}' resolved from the case-variant state record '${region}' (no record spells '${producerRegion}' exactly).`);
69169
+ return recovered;
69170
+ }
69171
+ }
69172
+ return;
68901
69173
  } catch (err) {
68902
69174
  logger.debug(`${prefix}: state read failed for Fn::GetStackOutput '${producerStack}.${outputName}' (${producerRegion}): ${err instanceof Error ? err.message : String(err)}`);
68903
69175
  return;
@@ -68937,6 +69209,7 @@ var S3LocalStateProvider = class {
68937
69209
  const loaded = await loadStateForStack(stackName, synthRegion, {
68938
69210
  statePrefix: this.opts.statePrefix,
68939
69211
  ...this.opts.stackRegion !== void 0 && { stackRegion: this.opts.stackRegion },
69212
+ ...this.opts.rawStackRegion !== void 0 && { rawStackRegion: this.opts.rawStackRegion },
68940
69213
  ...this.opts.stateBucket !== void 0 && { stateBucket: this.opts.stateBucket },
68941
69214
  ...this.opts.region !== void 0 && { region: this.opts.region },
68942
69215
  ...this.opts.profile !== void 0 && { profile: this.opts.profile }
@@ -69003,12 +69276,16 @@ var S3LocalStateProvider = class {
69003
69276
  */
69004
69277
  const fromStateFactory = (options) => {
69005
69278
  const opts = options;
69279
+ const clientRegion = canonicalizeRegion(opts.region) || void 0;
69280
+ const stackRegion = canonicalizeRegion(opts.stackRegion) || void 0;
69281
+ const rawStackRegion = opts.rawStackRegion || opts.stackRegion || void 0;
69006
69282
  return new S3LocalStateProvider({
69007
69283
  statePrefix: opts.statePrefix,
69008
69284
  ...opts.stateBucket !== void 0 && { stateBucket: opts.stateBucket },
69009
- ...opts.region !== void 0 && { region: opts.region },
69285
+ ...clientRegion !== void 0 && { region: clientRegion },
69010
69286
  ...opts.profile !== void 0 && { profile: opts.profile },
69011
- ...opts.stackRegion !== void 0 && { stackRegion: opts.stackRegion }
69287
+ ...stackRegion !== void 0 && { stackRegion },
69288
+ ...rawStackRegion !== void 0 && { rawStackRegion }
69012
69289
  });
69013
69290
  };
69014
69291
  /**
@@ -72190,6 +72467,10 @@ async function localStartApiCommand(target, options) {
72190
72467
  const logger = getLogger();
72191
72468
  if (options.verbose) logger.setLevel("debug");
72192
72469
  if (options.region !== void 0) options.region = canonicalizeRegion(options.region);
72470
+ if (options.stackRegion !== void 0) {
72471
+ options.rawStackRegion = options.stackRegion;
72472
+ options.stackRegion = canonicalizeRegion(options.stackRegion);
72473
+ }
72193
72474
  let apiFilter = target;
72194
72475
  if (options.api !== void 0) {
72195
72476
  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.`);
@@ -74532,6 +74813,10 @@ async function localRunTaskCommand(target, options) {
74532
74813
  if (options.verbose) logger.setLevel("debug");
74533
74814
  warnIfDeprecatedRegion(options);
74534
74815
  if (options.region !== void 0) options.region = canonicalizeRegion(options.region);
74816
+ if (options.stackRegion !== void 0) {
74817
+ options.rawStackRegion = options.stackRegion;
74818
+ options.stackRegion = canonicalizeRegion(options.stackRegion);
74819
+ }
74535
74820
  const state = createEcsRunState();
74536
74821
  let sigintHandler;
74537
74822
  let sigintCount = 0;
@@ -74576,12 +74861,12 @@ async function localRunTaskCommand(target, options) {
74576
74861
  const { stacks } = await synthesizer.synthesize(synthOpts);
74577
74862
  const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
74578
74863
  stateProvider = createLocalStateProvider$1(options, candidate?.stackName ?? "", candidate?.region);
74579
- const imageContext = await buildEcsImageResolutionContext$1(candidate, stateProvider, options);
74864
+ const { context: imageContext, stateRecordRegion } = await buildEcsImageResolutionContext$1(candidate, stateProvider, options);
74580
74865
  const task = resolveEcsTaskTarget(target, stacks, imageContext);
74581
74866
  logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
74582
74867
  const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
74583
74868
  if (stateProvider && taskNeeds.needsCrossStackResolver) {
74584
- const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? task.stack.region ?? "us-east-1";
74869
+ const consumerRegion = resolveEcsConsumerRegion(stateRecordRegion, options, task.stack.region);
74585
74870
  const resolver = await stateProvider.buildCrossStackResolver(consumerRegion);
74586
74871
  if (resolver) await applyCrossStackResolverToTask(task, {
74587
74872
  resources: imageContext?.stateResources ?? {},
@@ -74691,9 +74976,48 @@ async function assumeTaskRole(roleArn, region) {
74691
74976
  }
74692
74977
  }
74693
74978
  /**
74979
+ * Region the cross-stack resolver is scoped to for `cdkd local run-task`
74980
+ * (issue #1836).
74981
+ *
74982
+ * This value does THREE things (round 3 said TWO and was wrong — corrected in
74983
+ * round 4), and the first is why the state record's own spelling outranks the env
74984
+ * chain:
74985
+ *
74986
+ * 1. it becomes the exports-index KEY `cdkd/_index/{region}/exports.json`;
74987
+ * 2. it is the same-region filter of the index-miss per-stack scan; and
74988
+ * 3. via `SubstitutionContext.consumerRegion`, it is cdk-local's DEFAULT producer
74989
+ * region for an `Fn::GetStackOutput` that carries no explicit `Region` — i.e.
74990
+ * it becomes the `cdkd/{producer}/{region}/state.json` key that intrinsic
74991
+ * reads.
74992
+ *
74993
+ * All three land in `buildCrossStackResolver` (`local-state-loader.ts`), whose
74994
+ * `resolveImport` scan (2) and — since round 4 — `resolveGetStackOutput` (3) fold
74995
+ * BOTH sides for the comparison / recovery, while (1) is deliberately raw.
74996
+ *
74997
+ * `local invoke` / `local invoke-agentcore` both pass `loaded.region` there; this
74998
+ * command passed a FOLDED env-chain value instead, so the two commands keyed one
74999
+ * index differently — and a key the state records do not spell makes
75000
+ * `ExportIndexStore`'s rebuild filter match zero refs and PUT an EMPTY index,
75001
+ * permanently degrading every `Fn::ImportValue` to the O(N) scan. Keyed by a
75002
+ * record's spelling the filter always matches.
75003
+ *
75004
+ * The env-chain arm remains for the case where no state record was loaded at all
75005
+ * (nothing in the task needed one), and it stays FOLDED for the reason the rest
75006
+ * of this file folds: both env-var links escape the handler-entry `--region`
75007
+ * fold, and a raw upper-cased spelling reaches an S3 key and an SDK client.
75008
+ *
75009
+ * @internal exported for unit tests.
75010
+ */
75011
+ function resolveEcsConsumerRegion(stateRecordRegion, options, synthRegion) {
75012
+ if (stateRecordRegion) return stateRecordRegion;
75013
+ return canonicalizeRegion((options.region || process.env["AWS_REGION"]) ?? process.env["AWS_DEFAULT_REGION"] ?? synthRegion ?? "us-east-1");
75014
+ }
75015
+ /**
74694
75016
  * Build the substitution context the ECS task resolver consumes (issue
74695
- * #264). Returns `undefined` when no container's `Image` field needs
74696
- * substitution — the resolver behaves as before in that case.
75017
+ * #264), plus the region of the state record it loaded (if any — issue #1836;
75018
+ * see {@link resolveEcsConsumerRegion} for what reads it). `context` is
75019
+ * `undefined` when no container's `Image` field needs substitution — the
75020
+ * resolver behaves as before in that case.
74697
75021
  *
74698
75022
  * Tier 1 (pseudo parameters) fires `sts:GetCallerIdentity` once for
74699
75023
  * `${AWS::AccountId}`; region / partition / URL suffix come from the CLI
@@ -74712,10 +75036,11 @@ async function assumeTaskRole(roleArn, region) {
74712
75036
  */
74713
75037
  async function buildEcsImageResolutionContext$1(candidate, stateProvider, options) {
74714
75038
  const logger = getLogger();
74715
- if (!candidate) return void 0;
75039
+ if (!candidate) return {};
74716
75040
  const needs = detectEcsImageResolutionNeeds(candidate);
74717
- if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution && !needs.needsAssetRepoMarker) return;
75041
+ if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution && !needs.needsAssetRepoMarker) return {};
74718
75042
  const ctx = {};
75043
+ let stateRecordRegion;
74719
75044
  const wantsPseudoForEnvOrSecret = !!stateProvider && needs.needsEnvOrSecretSubstitution;
74720
75045
  if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
74721
75046
  const region = canonicalizeRegion(options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region);
@@ -74739,13 +75064,17 @@ async function buildEcsImageResolutionContext$1(candidate, stateProvider, option
74739
75064
  const wantsState = needs.needsStateResources || needs.needsEnvOrSecretSubstitution;
74740
75065
  if (stateProvider && wantsState) {
74741
75066
  const loaded = await stateProvider.load(candidate.stackName, candidate.region);
74742
- if (loaded) ctx.stateResources = loaded.resources;
75067
+ if (loaded) {
75068
+ ctx.stateResources = loaded.resources;
75069
+ stateRecordRegion = loaded.region;
75070
+ }
74743
75071
  } 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.");
74744
75072
  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).");
74745
75073
  if (needs.needsAssetRepoMarker && options.fromState) {
74746
75074
  const containerRepo = await loadBootstrapContainerRepo(candidate.region, {
74747
75075
  statePrefix: options.statePrefix,
74748
75076
  ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
75077
+ ...options.rawStackRegion !== void 0 && { rawStackRegion: options.rawStackRegion },
74749
75078
  ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
74750
75079
  ...options.region !== void 0 && { region: options.region },
74751
75080
  ...options.profile !== void 0 && { profile: options.profile },
@@ -74753,7 +75082,10 @@ async function buildEcsImageResolutionContext$1(candidate, stateProvider, option
74753
75082
  });
74754
75083
  if (containerRepo !== void 0) ctx.cdkAssetContainerRepo = containerRepo;
74755
75084
  }
74756
- return ctx;
75085
+ return {
75086
+ context: ctx,
75087
+ ...stateRecordRegion !== void 0 && { stateRecordRegion }
75088
+ };
74757
75089
  }
74758
75090
  function pickCandidateStack(stackPattern, stacks) {
74759
75091
  if (stackPattern === null) {
@@ -75186,6 +75518,10 @@ async function localInvokeAgentCoreCommand(target, options) {
75186
75518
  if (options.verbose) logger.setLevel("debug");
75187
75519
  warnIfDeprecatedRegion(options);
75188
75520
  if (options.region !== void 0) options.region = canonicalizeRegion(options.region);
75521
+ if (options.stackRegion !== void 0) {
75522
+ options.rawStackRegion = options.stackRegion;
75523
+ options.stackRegion = canonicalizeRegion(options.stackRegion);
75524
+ }
75189
75525
  let containerId;
75190
75526
  let stopLogs;
75191
75527
  let sigintHandler;
@@ -76076,16 +76412,37 @@ function emitA2aResult(result) {
76076
76412
  function platformToArchitecture(platform) {
76077
76413
  return platform === "linux/amd64" ? "x86_64" : "arm64";
76078
76414
  }
76415
+ /**
76416
+ * Copy the developer's AWS credentials + region into the container env for the
76417
+ * DEFAULT (no `--assume-role`) path.
76418
+ *
76419
+ * Issue [#1836](https://github.com/go-to-k/cdkd/issues/1836): the two REGION
76420
+ * entries are folded through `canonicalizeRegion` on the way in; the credentials
76421
+ * are copied verbatim (an AKID / session token is case-SENSITIVE, so lower-casing
76422
+ * the whole pass-through list would corrupt every one of them). The value lands
76423
+ * in the container's own `AWS_REGION`, i.e. in every SDK client the agent builds,
76424
+ * and SDK endpoint resolution is case-SENSITIVE (`CN-NORTH-1` resolves the
76425
+ * COMMERCIAL partition).
76426
+ *
76427
+ * Folding only inside `applyAgentCoreCredentialEnv`'s assume-role arm — which is
76428
+ * where issue #1814 stopped, and what round 3 of the #1836 review found still
76429
+ * open here after the same gap was closed in `local-invoke.ts` — made ONE command
76430
+ * yield two different container regions for the same shell: `cn-north-1` with
76431
+ * `--assume-role`, `CN-NORTH-1` without.
76432
+ */
76079
76433
  function forwardAwsEnv$1(env) {
76080
- for (const key of [
76434
+ const passThrough = [
76081
76435
  "AWS_ACCESS_KEY_ID",
76082
76436
  "AWS_SECRET_ACCESS_KEY",
76083
76437
  "AWS_SESSION_TOKEN",
76084
76438
  "AWS_REGION",
76085
76439
  "AWS_DEFAULT_REGION"
76086
- ]) {
76440
+ ];
76441
+ const regionKeys = /* @__PURE__ */ new Set(["AWS_REGION", "AWS_DEFAULT_REGION"]);
76442
+ for (const key of passThrough) {
76087
76443
  const value = process.env[key];
76088
- if (value !== void 0) env[key] = value;
76444
+ if (value === void 0) continue;
76445
+ env[key] = regionKeys.has(key) ? canonicalizeRegion(value) : value;
76089
76446
  }
76090
76447
  }
76091
76448
  async function assumeAgentCoreExecutionRole(roleArn, region) {
@@ -76388,6 +76745,10 @@ async function localInvokeCommand(target, options) {
76388
76745
  if (options.verbose) logger.setLevel("debug");
76389
76746
  warnIfDeprecatedRegion(options);
76390
76747
  if (options.region !== void 0) options.region = canonicalizeRegion(options.region);
76748
+ if (options.stackRegion !== void 0) {
76749
+ options.rawStackRegion = options.stackRegion;
76750
+ options.stackRegion = canonicalizeRegion(options.stackRegion);
76751
+ }
76391
76752
  let imagePlan;
76392
76753
  let containerId;
76393
76754
  let stopLogs;
@@ -76541,29 +76902,12 @@ async function localInvokeCommand(target, options) {
76541
76902
  AWS_LAMBDA_LOG_STREAM_NAME: "local",
76542
76903
  ...envResult.resolved
76543
76904
  };
76544
- let assumeSucceeded = false;
76545
- if (resolvedAssumeRoleArn) {
76546
- const stsRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
76547
- try {
76548
- const creds = await assumeLambdaExecutionRole(resolvedAssumeRoleArn, stsRegion);
76549
- dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
76550
- dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
76551
- dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
76552
- if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
76553
- assumeSucceeded = true;
76554
- } catch (err) {
76555
- const reason = err instanceof Error ? err.message : String(err);
76556
- logger.warn(`--assume-role: STS AssumeRole(${resolvedAssumeRoleArn}) failed: ${reason}. Falling back to the developer's shell credentials.`);
76557
- }
76558
- }
76559
- if (!assumeSucceeded) {
76560
- forwardAwsEnv(dockerEnv);
76561
- applyProfileCredentialsOverlay(dockerEnv, profileCredentials, false);
76562
- if (profileCredsFile) {
76563
- dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = profileCredsFile.containerPath;
76564
- dockerEnv["AWS_PROFILE"] = profileCredsFile.profileName;
76565
- }
76566
- }
76905
+ await applyLambdaCredentialEnv(dockerEnv, {
76906
+ ...resolvedAssumeRoleArn && { assumeRoleArn: resolvedAssumeRoleArn },
76907
+ ...options.region !== void 0 && { region: options.region },
76908
+ ...profileCredentials && { profileCredentials },
76909
+ ...profileCredsFile && { profileCredsFile }
76910
+ });
76567
76911
  let debugPort;
76568
76912
  if (options.debugPort) {
76569
76913
  debugPort = Number(options.debugPort);
@@ -76963,6 +77307,55 @@ async function readStdin() {
76963
77307
  return Buffer.concat(chunks).toString("utf-8");
76964
77308
  }
76965
77309
  /**
77310
+ * Inject AWS credentials into the container env. Precedence:
77311
+ * 1. `--assume-role` → STS-issued temp creds for the resolved ARN (on
77312
+ * STS failure, warn + fall through to dev creds). Swapping the
77313
+ * developer's credentials for the function's deployed execution role
77314
+ * degrades rather than hard-errors: this is a developer-loop tool, not
77315
+ * a security boundary, and the most common cause is the role's
77316
+ * `AssumeRolePolicy` not trusting the developer's IAM principal (a
77317
+ * config gap, not a cdkd bug).
77318
+ * 2. dev shell creds (`forwardAwsEnv` — which folds the region case for the
77319
+ * same reason arm 1 does) + `--profile` overlay
77320
+ * ({@link applyProfileCredentialsOverlay}) + the bind-mounted
77321
+ * credentials-file env so handler `fromIni({ profile })` resolves.
77322
+ * Applied in that order: the overlay intentionally wins over whatever
77323
+ * `forwardAwsEnv` copied, and the credentials-file env is additive.
77324
+ *
77325
+ * Extracted from `localInvokeCommand`'s body by issue
77326
+ * [#1836](https://github.com/go-to-k/cdkd/issues/1836) and shaped to mirror
77327
+ * `applyAgentCoreCredentialEnv` in `local-invoke-agentcore.ts` (the sibling
77328
+ * issue [#1814](https://github.com/go-to-k/cdkd/issues/1814) fixed), so a unit
77329
+ * test can lock the STS-client region binding with a mocked STS instead of
77330
+ * driving the full synth + docker pipeline.
77331
+ */
77332
+ async function applyLambdaCredentialEnv(dockerEnv, args) {
77333
+ const logger = getLogger();
77334
+ let assumeSucceeded = false;
77335
+ if (args.assumeRoleArn) {
77336
+ const stsRegion = canonicalizeRegion(args.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"]);
77337
+ try {
77338
+ const creds = await assumeLambdaExecutionRole(args.assumeRoleArn, stsRegion);
77339
+ dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
77340
+ dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
77341
+ dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
77342
+ if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
77343
+ assumeSucceeded = true;
77344
+ } catch (err) {
77345
+ const reason = err instanceof Error ? err.message : String(err);
77346
+ logger.warn(`--assume-role: STS AssumeRole(${args.assumeRoleArn}) failed: ${reason}. Falling back to the developer's shell credentials.`);
77347
+ }
77348
+ }
77349
+ if (!assumeSucceeded) {
77350
+ forwardAwsEnv(dockerEnv);
77351
+ applyProfileCredentialsOverlay(dockerEnv, args.profileCredentials, false);
77352
+ if (args.profileCredsFile) {
77353
+ dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = args.profileCredsFile.containerPath;
77354
+ dockerEnv["AWS_PROFILE"] = args.profileCredsFile.profileName;
77355
+ }
77356
+ }
77357
+ }
77358
+ /**
76966
77359
  * Assume the Lambda execution role and return temporary credentials.
76967
77360
  *
76968
77361
  * Closes the "developer has admin creds, the deployed function has narrow
@@ -77002,17 +77395,30 @@ async function assumeLambdaExecutionRole(roleArn, region) {
77002
77395
  *
77003
77396
  * Region is inherited from `AWS_REGION` / `AWS_DEFAULT_REGION` so
77004
77397
  * `aws.config.region` inside the handler works without extra setup.
77398
+ *
77399
+ * Issue [#1836](https://github.com/go-to-k/cdkd/issues/1836): the two REGION
77400
+ * entries are folded through `canonicalizeRegion` on the way in, the credentials
77401
+ * are copied verbatim. This is the DEFAULT path — no `--assume-role` — and it
77402
+ * carries the same exposure the assume-role branch had: the value lands in the
77403
+ * container's own `AWS_REGION`, i.e. in every SDK client the handler builds, and
77404
+ * SDK endpoint resolution is case-SENSITIVE (`CN-NORTH-1` resolves the
77405
+ * COMMERCIAL partition). Folding only inside `applyLambdaCredentialEnv`'s
77406
+ * assume-role arm made ONE command yield two different container regions for the
77407
+ * same shell — `cn-north-1` with `--assume-role`, `CN-NORTH-1` without.
77005
77408
  */
77006
77409
  function forwardAwsEnv(env) {
77007
- for (const key of [
77410
+ const passThrough = [
77008
77411
  "AWS_ACCESS_KEY_ID",
77009
77412
  "AWS_SECRET_ACCESS_KEY",
77010
77413
  "AWS_SESSION_TOKEN",
77011
77414
  "AWS_REGION",
77012
77415
  "AWS_DEFAULT_REGION"
77013
- ]) {
77416
+ ];
77417
+ const regionKeys = /* @__PURE__ */ new Set(["AWS_REGION", "AWS_DEFAULT_REGION"]);
77418
+ for (const key of passThrough) {
77014
77419
  const value = process.env[key];
77015
- if (value !== void 0) env[key] = value;
77420
+ if (value === void 0) continue;
77421
+ env[key] = regionKeys.has(key) ? canonicalizeRegion(value) : value;
77016
77422
  }
77017
77423
  }
77018
77424
  /**
@@ -78258,7 +78664,7 @@ function createMigrateCommand() {
78258
78664
  */
78259
78665
  function buildProgram() {
78260
78666
  const program = new Command();
78261
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.12");
78667
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.14");
78262
78668
  program.hook("preAction", (_thisCommand, actionCommand) => {
78263
78669
  const { profile } = actionCommand.optsWithGlobals();
78264
78670
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;