@go-to-k/cdkd 0.284.3 → 0.284.4

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.
@@ -1045,7 +1045,7 @@ var aws_clients_exports = /* @__PURE__ */ __exportAll({
1045
1045
  /**
1046
1046
  * AWS clients manager
1047
1047
  */
1048
- var AwsClients = class {
1048
+ var AwsClients = class AwsClients {
1049
1049
  s3Client;
1050
1050
  cloudControlClient;
1051
1051
  iamClient;
@@ -1080,6 +1080,87 @@ var AwsClients = class {
1080
1080
  };
1081
1081
  }
1082
1082
  /**
1083
+ * The region this instance was EXPLICITLY configured with, or `undefined`.
1084
+ *
1085
+ * Deliberately reads only `config.region` and consults NO environment
1086
+ * variable, which is the opposite of what an earlier revision did and the
1087
+ * difference matters (issue #1957). An env read here is not merely
1088
+ * incomplete, it is UNSTABLE in the one direction that is dangerous: the SDK
1089
+ * memoizes a region-less client's region at its first resolution
1090
+ * (`@smithy/node-config-provider`'s `loadConfig` wraps the provider chain in
1091
+ * `memoize`), while `deploy.ts`'s `switchRegion` keeps mutating
1092
+ * `process.env.AWS_REGION` per stack and restores it in each stack's
1093
+ * `finally`. So an env-derived answer can report region X for a client that
1094
+ * long ago pinned itself to region P — letting a caller conclude "these
1095
+ * clients already point at my region" and use the WRONG ones, which is worse
1096
+ * than not knowing.
1097
+ *
1098
+ * When this returns `undefined` the region is not merely unknown to us, it is
1099
+ * NOT YET DECIDED — and that is the important part. {@link clientOptions}
1100
+ * omits `region` entirely in that case, so each service client resolves and
1101
+ * MEMOIZES its own region independently, at its own first construction, from
1102
+ * an environment `deploy.ts`'s `switchRegion` is actively mutating. The
1103
+ * members of one region-less bag can therefore disagree with each other:
1104
+ * `ssm` can pin `us-west-2` and `secretsManager` pin `us-east-1` a moment
1105
+ * later, because the getters are lazy and each samples a different instant.
1106
+ *
1107
+ * So there is deliberately NO method here that reports "the region of these
1108
+ * clients" for an unconfigured instance. An earlier revision had one — it
1109
+ * asked `this.ssm.config.region()` — and it was unsound for exactly this
1110
+ * reason: it measured ONE member of a bag whose members need not agree, and
1111
+ * the caller then reused the whole bag on the strength of it. A caller that
1112
+ * needs a region it can rely on must build a CONFIGURED bag
1113
+ * ({@link withRegion} always sets `region`, so every member of a derived bag
1114
+ * agrees by construction).
1115
+ */
1116
+ get configuredRegion() {
1117
+ return this.config.region || void 0;
1118
+ }
1119
+ /**
1120
+ * The CREDENTIAL half of this instance's configuration — `profile` plus any
1121
+ * explicitly supplied `credentials` — deliberately WITHOUT `region`.
1122
+ *
1123
+ * This is the half that must survive a region override. Note what it is and
1124
+ * is NOT worth: `--profile` ALSO reaches a freshly constructed client through
1125
+ * the environment, because `src/cli/program.ts` sets `process.env.AWS_PROFILE`
1126
+ * in a `preAction` hook for every command — so carrying `profile` here is
1127
+ * belt-and-braces rather than the thing standing between a user and the wrong
1128
+ * account. What genuinely has no environment path is an explicit
1129
+ * `credentials` object: nothing exports it, so a sibling built without it
1130
+ * falls back to the default chain. The same is true of any library caller
1131
+ * that constructs `AwsClients` directly and therefore never runs the CLI's
1132
+ * `preAction` hook. `--role-arn` needs nothing carried at all —
1133
+ * `applyRoleArnIfSet` exports `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` /
1134
+ * `AWS_SESSION_TOKEN` into the process environment.
1135
+ *
1136
+ * The `credentials` object is CLONED rather than aliased: the returned bag is
1137
+ * handed to every derived sibling, and sharing one mutable object would let a
1138
+ * caller reach through it and rewrite the ambient instance's credentials.
1139
+ */
1140
+ get credentialConfig() {
1141
+ return {
1142
+ ...this.config.profile && { profile: this.config.profile },
1143
+ ...this.config.credentials && { credentials: { ...this.config.credentials } }
1144
+ };
1145
+ }
1146
+ /**
1147
+ * Derive a sibling bound to `region`, carrying this instance's credential
1148
+ * configuration (see {@link credentialConfig}) and overriding ONLY the region.
1149
+ *
1150
+ * Used by {@link IntrinsicFunctionResolver} to pin a dynamic-reference lookup
1151
+ * (`{{resolve:secretsmanager:...}}` / `{{resolve:ssm:...}}`) to the stack's own
1152
+ * region instead of whichever region the process-global singleton happens to
1153
+ * hold at that moment (issue #1957). The returned instance owns its own lazily
1154
+ * constructed clients and its own `destroy()`; it is NOT registered as the
1155
+ * global, so nothing else in the process can observe it.
1156
+ */
1157
+ withRegion(region) {
1158
+ return new AwsClients({
1159
+ ...this.credentialConfig,
1160
+ region
1161
+ });
1162
+ }
1163
+ /**
1083
1164
  * Get S3 client
1084
1165
  *
1085
1166
  * Note: If region and credentials are not provided, AWS SDK will use:
@@ -12283,6 +12364,44 @@ const MAX_DYNAMIC_REFERENCE_THROTTLE_RETRIES = 4;
12283
12364
  * without real waits (mirrors `describeTypeRetryDelays`).
12284
12365
  */
12285
12366
  const dynamicReferenceRetryDelays = {};
12367
+ /**
12368
+ * Is `region` safe to build an AWS SDK client from?
12369
+ *
12370
+ * This is a SECURITY gate, not an AWS region registry, and the distinction
12371
+ * decides how strict it is. The SDK turns a region into a hostname by
12372
+ * substitution — `https://ssm.{region}.amazonaws.com` — so a value carrying a
12373
+ * host delimiter escapes the label and re-points the endpoint: the measured
12374
+ * case is `evil.example.com#`, which yields
12375
+ * `https://ssm.evil.example.com/#.amazonaws.com` and sends a SigV4-SIGNED
12376
+ * request (access key id + signature) to an attacker-controlled host.
12377
+ *
12378
+ * The reachable input is `Fn::GetAZs`, whose argument is TEMPLATE-DERIVED and
12379
+ * can arrive through an `Fn::ImportValue` or a parameter — i.e. it is not
12380
+ * necessarily written by whoever runs the deploy. Before issue #1957 that value
12381
+ * only fed the `region-name` FILTER of a `DescribeAvailabilityZones` call and
12382
+ * never built a client, so binding lookups to a region is exactly what made it
12383
+ * reachable; the gate ships with the binding.
12384
+ *
12385
+ * So the predicate is CHARSET-based rather than shape-based: lowercase
12386
+ * alphanumerics and hyphens only, which cannot express `.`, `/`, `:`, `@`, `?`
12387
+ * or `#` and therefore cannot leave the hostname label. It deliberately does
12388
+ * NOT try to enumerate real regions — AWS keeps adding them
12389
+ * (`ap-southeast-7`, `il-central-1`, `mx-central-1`, `eusc-de-east-1`), and a
12390
+ * pattern tight enough to reject `----` would also reject the next one. A
12391
+ * region-shaped-but-nonexistent value is not a security problem: it resolves to
12392
+ * a hostname that does not exist and the SDK fails loudly.
12393
+ *
12394
+ * Note the sibling pattern in `src/cli/commands/state-file-keys.ts` is NOT
12395
+ * reusable here: it requires `^[a-z]{2}(-[a-z]+)+-\d+$`, which rejects
12396
+ * `eusc-de-east-1` (the European Sovereign Cloud partition's four-letter
12397
+ * prefix).
12398
+ *
12399
+ * Callers must {@link canonicalizeRegion} first — `US-EAST-1` is a documented
12400
+ * input and is lowercase-canonical, not invalid.
12401
+ */
12402
+ function isClientSafeRegion(region) {
12403
+ return /^[a-z0-9][a-z0-9-]{0,30}$/.test(region);
12404
+ }
12286
12405
  /** Test seam for {@link FABRICATED_ACCOUNT_INFO_TTL_MS} expiry. */
12287
12406
  const accountInfoClock = { now: () => Date.now() };
12288
12407
  let fabricatedAccountIdentity = null;
@@ -12421,6 +12540,63 @@ function stringifyParameterForLog(paramDef, value) {
12421
12540
  var IntrinsicFunctionResolver = class {
12422
12541
  logger = getLogger().child("IntrinsicFunctionResolver");
12423
12542
  resolverRegion;
12543
+ /**
12544
+ * The region the CONSTRUCTOR was given, or `undefined` when it was called
12545
+ * without one — unlike {@link resolverRegion}, which substitutes
12546
+ * `AWS_REGION` / `us-east-1` so every consumer has a string to work with.
12547
+ *
12548
+ * The distinction is load-bearing for {@link clientsForRegion} and for
12549
+ * nothing else: re-pointing an AWS lookup away from the ambient clients is
12550
+ * only safe when a caller SAID which region this resolver stands for.
12551
+ *
12552
+ * IN PRODUCTION THIS IS ALWAYS SET, and saying so matters more than the
12553
+ * guard it enables. Every construction site defaults the region BEFORE the
12554
+ * constructor and passes a `string` — `deploy.ts`, `scrub.ts`, `drift.ts`,
12555
+ * `import.ts`, `export.ts`, `diff-recursive.ts`, `rollback-executor.ts` —
12556
+ * so the `undefined` arm is reachable only through the no-argument
12557
+ * constructor, which nothing but tests uses. It is kept because the
12558
+ * parameter is optional and the arm must therefore exist, not because a
12559
+ * shipped path depends on it.
12560
+ *
12561
+ * That has a USER-VISIBLE consequence, deliberately accepted (issue #1957
12562
+ * review). For a REGION-AGNOSTIC stack (no `env.region`) run with neither
12563
+ * `--region` nor `AWS_REGION`, the region those callers compute is the
12564
+ * hard-coded `us-east-1` fallback, so the lookup now goes there — where
12565
+ * before it followed the ambient clients to whatever `~/.aws/config` said.
12566
+ * The new behaviour is the consistent one: `us-east-1` is already the region
12567
+ * cdkd keys that stack's state file, its lock and its export index under, so
12568
+ * the resolved value and the record that stores it now agree. Previously
12569
+ * they did not, which is the same class of defect this issue is about, one
12570
+ * layer up. A stack WITH an explicit `env.region` is unaffected — every
12571
+ * caller prefers it (`scrub.ts` does `stack.region || region`).
12572
+ */
12573
+ explicitRegion;
12574
+ /**
12575
+ * AWS clients pinned to a region OTHER than the ambient singleton's, built
12576
+ * lazily on first mismatch and keyed by region (issue #1957).
12577
+ *
12578
+ * Lifetime is deliberately the resolver's own, matching {@link cfnClients}
12579
+ * two fields down: both are per-region SDK clients this instance builds for
12580
+ * itself, and neither is destroyed, because `IntrinsicFunctionResolver` has
12581
+ * no teardown hook and every construction site (`DeployEngine`, `scrub`,
12582
+ * `drift`, `import`, `diff-recursive`, `export`, `rollback-executor`) would
12583
+ * have to grow one. The bound on what that costs is small and worth stating:
12584
+ * an entry exists only when a resolver's region DIFFERS from the ambient
12585
+ * one — i.e. only on a genuinely cross-region run — and at most one per
12586
+ * foreign region per resolver, versus the ambient clients which are already
12587
+ * created and destroyed per stack by `deploy.ts`.
12588
+ *
12589
+ * KEYED BY REGION ALONE, which means an entry also pins the CREDENTIAL
12590
+ * configuration of whichever ambient instance was current at the first
12591
+ * mismatch for that region. That is sound today because the credential half
12592
+ * is process-wide rather than per-stack: `--profile` comes from one CLI
12593
+ * option and `--role-arn` lands in `process.env`, so every ambient instance
12594
+ * a run installs carries the same one. Widen the key to include
12595
+ * {@link AwsClients.credentialConfig} the moment that stops being true —
12596
+ * per-stack credentials would otherwise let one stack's lookups run under
12597
+ * another's identity, which is a worse bug than the one this cache serves.
12598
+ */
12599
+ regionScopedClients = /* @__PURE__ */ new Map();
12424
12600
  strictGetAtt;
12425
12601
  cfnFallback;
12426
12602
  /**
@@ -12511,24 +12687,17 @@ var IntrinsicFunctionResolver = class {
12511
12687
  * nested stacks ship or that pass starts recording secrets; a resolver per
12512
12688
  * node is the fix then, not a wider key here.
12513
12689
  *
12514
- * What this does NOT settleissue
12515
- * [#1957](https://github.com/go-to-k/cdkd/issues/1957) owns it: the lookups
12516
- * themselves still go through the process-ambient `getAwsClients()` singleton
12517
- * (see `resolveSecretsManagerReference` / `resolveSSMReference`), whose region
12518
- * is whichever the process installed last. So a resolver constructed for
12519
- * region B while the ambient clients point at region A still reads A on its
12520
- * FIRST resolution no cache involved, so nothing here can prevent it.
12521
- * `cdkd deploy` re-pins the singleton per stack, which makes a SERIAL
12522
- * multi-region deploy correct end to end, but the default
12523
- * `--stack-concurrency 4` races for it (a hazard `deploy.ts` already
12524
- * documents) and `cdkd scrub` installs its clients once while resolving
12525
- * stacks in several regions. The split of ownership is therefore: THIS field
12526
- * closes the cache as a cross-region / cross-stack carrier, while #1957 owns
12527
- * the wrong-region READ — which is the outcome #1933's title names, so #1933
12528
- * is not fully resolved until #1957 lands. Pinning the lookup to
12529
- * {@link resolverRegion} means constructing region-scoped clients, which is a
12530
- * credentials decision (a bare `new SSMClient({ region })` drops the ambient
12531
- * profile / assume-role config) and belongs to #1957 rather than here.
12690
+ * The OTHER half of the same outcome the lookups themselves reading the
12691
+ * process-ambient `getAwsClients()` singleton, whose region is whichever the
12692
+ * process installed last was issue
12693
+ * [#1957](https://github.com/go-to-k/cdkd/issues/1957) and is now closed by
12694
+ * {@link clientsForRegion}: a resolver whose region differs from the ambient
12695
+ * one builds its own region-pinned clients (carrying the ambient profile /
12696
+ * credentials) instead of reading whatever the singleton currently holds.
12697
+ * The two halves remain SEPARATE mechanisms and both are needed — this field
12698
+ * stops a resolved value from travelling between regions or stacks, while
12699
+ * the scoped clients stop the FIRST resolution from reading the wrong region
12700
+ * (no cache involved, so nothing here could ever have prevented it).
12532
12701
  */
12533
12702
  cachedDynamicReferences = /* @__PURE__ */ new Map();
12534
12703
  /**
@@ -12554,6 +12723,7 @@ var IntrinsicFunctionResolver = class {
12554
12723
  warnedUnrecognizedSsmTypes = /* @__PURE__ */ new Set();
12555
12724
  constructor(region, options) {
12556
12725
  this.resolverRegion = region || process.env["AWS_REGION"] || "us-east-1";
12726
+ this.explicitRegion = region || void 0;
12557
12727
  this.strictGetAtt = options?.strictGetAtt ?? false;
12558
12728
  this.cfnFallback = options?.cfnFallback ?? true;
12559
12729
  }
@@ -12566,6 +12736,133 @@ var IntrinsicFunctionResolver = class {
12566
12736
  this.physicalIdFallbackCount = 0;
12567
12737
  }
12568
12738
  /**
12739
+ * AWS clients for a REGION-SENSITIVE lookup, pinned to `targetRegion`
12740
+ * (issue [#1957](https://github.com/go-to-k/cdkd/issues/1957)).
12741
+ *
12742
+ * Every lookup in this class used to read `getAwsClients()` — the
12743
+ * PROCESS-GLOBAL singleton, whose region is whichever one the process
12744
+ * installed last. That is not the same thing as the region this resolver
12745
+ * stands for, and the gap is reachable on main:
12746
+ *
12747
+ * - `cdkd deploy` defaults to `--stack-concurrency 4` and re-points the
12748
+ * singleton per stack, so two stacks in different regions race for one
12749
+ * mutable global and stack B's `GetSecretValue` / `GetParameter` can run
12750
+ * against stack A's client. The resolved value is redacted on its way into
12751
+ * state, so nothing downstream records which region answered.
12752
+ * - `cdkd scrub --all` installs the clients ONCE while resolving per-stack
12753
+ * regions, so a region-B `SecureString` whose region-A namesake is a plain
12754
+ * `String` is classified PUBLIC and left in PLAINTEXT in state.json — the
12755
+ * same disclosure class as GHSA-p5qg-v9gv-hc7w, not merely a wrong value.
12756
+ * - `cdkd drift --revert` WRITES the resolved value to a live resource, so
12757
+ * there the wrong region is a wrong write rather than a wrong report.
12758
+ *
12759
+ * Fixing it here rather than at the ~10 `setAwsClients` call sites is what
12760
+ * makes it one mechanism instead of a per-command patch: this class already
12761
+ * knows its own region, and every construction site already passes the
12762
+ * per-stack one.
12763
+ *
12764
+ * REUSING THE AMBIENT CLIENTS REQUIRES PROOF THAT THEY ALREADY POINT AT
12765
+ * `targetRegion`, and the direction of that test is the whole correctness
12766
+ * argument. CloudFormation semantics say a stack's dynamic references resolve
12767
+ * in the STACK's region, and every construction site passes exactly that — so
12768
+ * once a region has been named, sending the lookup there is not an
12769
+ * optimisation to be justified, it is the requirement. Whether the ambient
12770
+ * singleton happens to agree only decides whether an object allocation can be
12771
+ * skipped.
12772
+ *
12773
+ * An earlier revision had this backwards twice over, and both failures are
12774
+ * worth naming because each looks reasonable in isolation.
12775
+ *
12776
+ * It first declined to override whenever the ambient region was UNKNOWN,
12777
+ * reasoning that overriding on an unproven mismatch might re-point a lookup
12778
+ * that works today. That fails OPEN, on the COMMON configuration: `aws
12779
+ * configure` writes the region to `~/.aws/config`, and `cdkd scrub` sets a
12780
+ * client region only when `--region` is passed. The disclosure this issue
12781
+ * exists to close stayed reachable — profile region `us-east-1`, stack B in
12782
+ * `ap-northeast-1`, a name that is `String` in A and `SecureString` in B,
12783
+ * `cdkd scrub --all` with no flags: B's reference answered by A, classified
12784
+ * public, plaintext left in `state.json`.
12785
+ *
12786
+ * It then determined the ambient region by reading `process.env` here, which
12787
+ * is worse than not knowing: the SDK memoizes a region-less client's region
12788
+ * at its first resolution while `deploy.ts`'s `switchRegion` keeps mutating
12789
+ * `AWS_REGION` per stack and restores it in each stack's `finally`, so the
12790
+ * environment could say `baseRegion` for a client long since pinned
12791
+ * elsewhere — and this method would conclude MATCH and hand back clients
12792
+ * pointing somewhere else.
12793
+ *
12794
+ * The fix for THAT was to ask the SDK (`ssm.config.region()`), and it was
12795
+ * still wrong, in a way worth writing down because it looks airtight. An
12796
+ * unconfigured `AwsClients` is not a bag of clients, it is a bag of DEFERRED
12797
+ * client constructions: `clientOptions` omits `region`, the getters are lazy,
12798
+ * and each member therefore samples the mutating environment at its own
12799
+ * instant and memoizes a possibly DIFFERENT region. Asking `ssm` measures one
12800
+ * member and says nothing about `secretsManager`, so the seam could short-
12801
+ * circuit on a us-west-2 `ssm` and then hand out a bag whose `secretsManager`
12802
+ * pins us-east-1 a moment later — issue #1957's Site 1 surviving inside the
12803
+ * arm meant to fix it.
12804
+ *
12805
+ * So the short-circuit is taken ONLY when the ambient's region is
12806
+ * CONFIGURED. That is not a heuristic: a configured bag passes `region` to
12807
+ * every member ({@link AwsClients.clientOptions}), so its members agree by
12808
+ * construction, and {@link AwsClients.withRegion} always sets one, so every
12809
+ * derived bag is internally consistent too. An unconfigured ambient is not
12810
+ * "of unknown region", it is "of not-yet-decided region", and there is
12811
+ * nothing to compare against — so it SCOPES. That is the same "unknown means
12812
+ * SCOPE, not skip" rule as above, applied one level deeper.
12813
+ *
12814
+ * Three arms return the ambient instance, each for a reason that is not
12815
+ * "we could not prove a mismatch":
12816
+ *
12817
+ * 1. No `targetRegion` — no region was ever named (see
12818
+ * {@link explicitRegion}), so there is nothing to bind to.
12819
+ * 2. `targetRegion` is not safe to build a client from (see
12820
+ * {@link isClientSafeRegion}) — which THROWS. An earlier revision warned
12821
+ * and fell back to the ambient clients, reasoning that a malformed region
12822
+ * reaching here is a cdkd bug and failing every lookup would turn it into
12823
+ * an outage. That put this arm on the wrong side of the two-severity
12824
+ * design: falling back to the ambient means READING ANOTHER REGION, which
12825
+ * for `scrub` / `drift` / `import` — whose region is state-derived — is
12826
+ * the disclosure this issue exists to close (a region-B `SecureString`
12827
+ * classified against a region-A `String`). A stopped command is strictly
12828
+ * better than a silent wrong-region read. The `Fn::GetAZs` entry still
12829
+ * validates EARLIER so it can give a message naming the template
12830
+ * construct; this arm is the backstop that guarantees no call site,
12831
+ * present or future, routes unvalidated input into an SDK endpoint.
12832
+ * 3. The installed clients cannot DERIVE a sibling — `withRegion` is absent.
12833
+ * In production that never happens: `getAwsClients()` returns an
12834
+ * `AwsClients`. It is true only of a test double, and it is checked
12835
+ * EXPLICITLY rather than left to emerge, for a reason the review of this
12836
+ * change made concrete. The ~260 suites that stub `getAwsClients()` with a
12837
+ * plain object used to stay on the ambient path as a side effect of the
12838
+ * `undefined`-region guard above — the very guard that made the disclosure
12839
+ * reachable. Removing that guard without putting something deliberate in
12840
+ * its place would have traded a security hole for ~260 `TypeError`s, so
12841
+ * the test-double case is now its own named arm and the security arm no
12842
+ * longer has a testing job to do. Suites that are ABOUT region scoping use
12843
+ * a real `AwsClients` and are unaffected by it.
12844
+ *
12845
+ * Regions are canonicalised on both sides before comparing, because
12846
+ * `--region US-EAST-1` is a documented input and the repo lowercases
12847
+ * elsewhere (`canonicalizeRegion`, issues #1795 / #1850). Without it an
12848
+ * uppercase spelling would build a second client for the same physical
12849
+ * region — benign, but wasteful and confusing in a debug log.
12850
+ */
12851
+ clientsForRegion(targetRegion) {
12852
+ const ambient = getAwsClients();
12853
+ if (!targetRegion) return ambient;
12854
+ const target = canonicalizeRegion(targetRegion);
12855
+ if (!isClientSafeRegion(target)) throw new Error(`Refusing to build AWS clients for the region '${stripControlChars(target).slice(0, 64)}': it is not a valid AWS region name, and a region is substituted into the AWS service hostname.`);
12856
+ if (typeof ambient.withRegion !== "function") return ambient;
12857
+ const cached = this.regionScopedClients.get(target);
12858
+ if (cached) return cached;
12859
+ if (canonicalizeRegion(ambient.configuredRegion) === target) return ambient;
12860
+ const scoped = ambient.withRegion(target);
12861
+ this.regionScopedClients.set(target, scoped);
12862
+ this.logger.debug(`Using region-scoped AWS clients for ${target}`);
12863
+ return scoped;
12864
+ }
12865
+ /**
12569
12866
  * Resolve parameter values from template Parameters section
12570
12867
  *
12571
12868
  * Merges default values from template with user-provided parameter values.
@@ -12617,7 +12914,7 @@ var IntrinsicFunctionResolver = class {
12617
12914
  * Used for parameters with type AWS::SSM::Parameter::Value<...>.
12618
12915
  */
12619
12916
  async resolveSSMParameter(parameterName) {
12620
- return (await getAwsClients().ssm.send(new GetParameterCommand({ Name: parameterName }))).Parameter?.Value ?? "";
12917
+ return (await this.clientsForRegion(this.explicitRegion).ssm.send(new GetParameterCommand({ Name: parameterName }))).Parameter?.Value ?? "";
12621
12918
  }
12622
12919
  /**
12623
12920
  * Coerce parameter value to the correct type based on parameter definition
@@ -13190,7 +13487,7 @@ var IntrinsicFunctionResolver = class {
13190
13487
  const cached = cachedEc2InstanceAttributes[cacheKey];
13191
13488
  if (cached !== void 0) return cached;
13192
13489
  try {
13193
- const instance = (await getAwsClients().ec2.send(new DescribeInstancesCommand({ InstanceIds: [physicalId] }))).Reservations?.[0]?.Instances?.[0];
13490
+ const instance = (await this.clientsForRegion(this.explicitRegion).ec2.send(new DescribeInstancesCommand({ InstanceIds: [physicalId] }))).Reservations?.[0]?.Instances?.[0];
13194
13491
  let value;
13195
13492
  switch (attributeName) {
13196
13493
  case "PrivateIp":
@@ -13224,7 +13521,7 @@ var IntrinsicFunctionResolver = class {
13224
13521
  if (resourceType === "AWS::EC2::LaunchTemplate") {
13225
13522
  if (attributeName === "LatestVersionNumber" || attributeName === "DefaultVersionNumber") {
13226
13523
  try {
13227
- const lt = (await getAwsClients().ec2.send(new DescribeLaunchTemplatesCommand({ LaunchTemplateIds: [physicalId] }))).LaunchTemplates?.[0];
13524
+ const lt = (await this.clientsForRegion(this.explicitRegion).ec2.send(new DescribeLaunchTemplatesCommand({ LaunchTemplateIds: [physicalId] }))).LaunchTemplates?.[0];
13228
13525
  const value = attributeName === "LatestVersionNumber" ? lt?.LatestVersionNumber : lt?.DefaultVersionNumber;
13229
13526
  if (value !== void 0 && value !== null) return String(value);
13230
13527
  } catch (err) {
@@ -13873,7 +14170,9 @@ var IntrinsicFunctionResolver = class {
13873
14170
  if ("Region" in args && args["Region"] !== void 0 && args["Region"] !== null) {
13874
14171
  const resolvedRegion = await this.resolveValue(args["Region"], context);
13875
14172
  if (typeof resolvedRegion !== "string" || resolvedRegion === "") throw new Error(`Fn::GetStackOutput: Region must resolve to a non-empty string, got ${typeof resolvedRegion}`);
13876
- region = resolvedRegion;
14173
+ const requestedRegion = canonicalizeRegion(resolvedRegion);
14174
+ if (!isClientSafeRegion(requestedRegion)) throw new Error(`Fn::GetStackOutput: '${stripControlChars(resolvedRegion).slice(0, 64)}' is not a valid AWS region name. The region selects both the AWS endpoint and the state-file key, so cdkd will not use it.`);
14175
+ region = requestedRegion;
13877
14176
  }
13878
14177
  let roleArn;
13879
14178
  if ("RoleArn" in args && args["RoleArn"] !== void 0 && args["RoleArn"] !== null) {
@@ -14056,28 +14355,52 @@ var IntrinsicFunctionResolver = class {
14056
14355
  async resolveGetAZs(value, context) {
14057
14356
  const resolvedValue = await this.resolveValue(value, context);
14058
14357
  let region;
14059
- if (typeof resolvedValue === "string" && resolvedValue !== "") region = resolvedValue;
14060
- else region = (await getAccountInfo(this.resolverRegion)).region;
14358
+ /**
14359
+ * Which region's clients answer the `DescribeAvailabilityZones` below.
14360
+ *
14361
+ * `DescribeAvailabilityZones` lists the AZs of the region the CLIENT is
14362
+ * pointed at; the `region-name` filter narrows that listing, it does not
14363
+ * widen it to another region. So a foreign-region client returns an EMPTY
14364
+ * list, which this method then caches and hands back as the resolved value
14365
+ * of `Fn::GetAZs` — silently, since an empty list is not an error here
14366
+ * (issue #1957).
14367
+ *
14368
+ * When the template names a region explicitly, THAT is the region to talk
14369
+ * to. Otherwise fall back to the resolver's own — but only when it was
14370
+ * given explicitly (see {@link explicitRegion}).
14371
+ */
14372
+ let clientRegion;
14373
+ if (typeof resolvedValue === "string" && resolvedValue !== "") {
14374
+ const requested = canonicalizeRegion(resolvedValue);
14375
+ if (!isClientSafeRegion(requested)) throw new Error(`Fn::GetAZs: '${stripControlChars(resolvedValue).slice(0, 64)}' is not a valid AWS region name. A region is substituted into the AWS service hostname, so cdkd will not build a client from it.`);
14376
+ region = requested;
14377
+ clientRegion = requested;
14378
+ } else {
14379
+ region = (await getAccountInfo(this.resolverRegion)).region;
14380
+ clientRegion = this.explicitRegion;
14381
+ }
14061
14382
  const cached = cachedAvailabilityZones[region];
14062
14383
  if (cached) {
14063
14384
  this.logger.debug(`Resolved Fn::GetAZs from cache: ${region} -> ${JSON.stringify(cached)}`);
14064
14385
  return cached;
14065
14386
  }
14066
- const ec2Client = getAwsClients().ec2;
14387
+ const ec2Client = this.clientsForRegion(clientRegion).ec2;
14388
+ let azNames;
14067
14389
  try {
14068
- const azNames = ((await ec2Client.send(new DescribeAvailabilityZonesCommand({ Filters: [{
14390
+ azNames = ((await ec2Client.send(new DescribeAvailabilityZonesCommand({ Filters: [{
14069
14391
  Name: "region-name",
14070
14392
  Values: [region]
14071
14393
  }, {
14072
14394
  Name: "state",
14073
14395
  Values: ["available"]
14074
14396
  }] }))).AvailabilityZones || []).map((az) => az.ZoneName).filter((name) => name !== void 0).sort();
14075
- cachedAvailabilityZones[region] = azNames;
14076
- this.logger.debug(`Resolved Fn::GetAZs: ${region} -> ${JSON.stringify(azNames)}`);
14077
- return azNames;
14078
14397
  } catch (error) {
14079
14398
  throw new Error(`Fn::GetAZs: failed to describe availability zones for region '${region}': ${error instanceof Error ? error.message : String(error)}`);
14080
14399
  }
14400
+ if (azNames.length === 0) throw new Error(`Fn::GetAZs: no availability zones returned for region '${region}'. Either the region is not enabled on this account (opt-in regions must be enabled before use), or the request was answered by a different region's endpoint.`);
14401
+ cachedAvailabilityZones[region] = azNames;
14402
+ this.logger.debug(`Resolved Fn::GetAZs: ${region} -> ${JSON.stringify(azNames)}`);
14403
+ return azNames;
14081
14404
  }
14082
14405
  /**
14083
14406
  * Resolve pseudo parameters
@@ -14226,7 +14549,7 @@ var IntrinsicFunctionResolver = class {
14226
14549
  if (!versionStage) versionStage = "AWSCURRENT";
14227
14550
  if (!secretId) throw new Error("Dynamic reference: secretsmanager SECRET_ID is required");
14228
14551
  this.logger.debug(`Resolving dynamic reference: secretsmanager:${secretId}:SecretString:${jsonKey}:${versionStage}:${versionId}`);
14229
- const client = getAwsClients().secretsManager;
14552
+ const client = this.clientsForRegion(this.explicitRegion).secretsManager;
14230
14553
  const command = new GetSecretValueCommand({
14231
14554
  SecretId: secretId,
14232
14555
  ...versionStage && versionStage !== "" && { VersionStage: versionStage },
@@ -14354,10 +14677,13 @@ var IntrinsicFunctionResolver = class {
14354
14677
  * - It says NOTHING about concurrency. The client is captured before the
14355
14678
  * first attempt, so a sibling stack's teardown (`stackAwsClients.destroy()`
14356
14679
  * in `deploy.ts`) during a backoff surfaces as a raw, non-throttle-shaped
14357
- * failure on the next attempt. That is a property of the ambient-singleton
14358
- * design this PR deliberately does not touch (issue
14359
- * [#1957](https://github.com/go-to-k/cdkd/issues/1957)), not something the
14360
- * retry makes safe.
14680
+ * failure on the next attempt, and the retry does not make that safe. Issue
14681
+ * [#1957](https://github.com/go-to-k/cdkd/issues/1957) NARROWED this rather
14682
+ * than removing it: a lookup whose region differs from the ambient one now
14683
+ * runs on {@link clientsForRegion}'s own clients, which no sibling stack can
14684
+ * destroy because nothing else in the process holds a reference to them. A
14685
+ * sibling in the SAME region still shares the ambient instance, so the
14686
+ * window survives exactly where the two stacks agree on the region.
14361
14687
  */
14362
14688
  sendWithThrottleRetry(operation, label) {
14363
14689
  return withRetry(operation, label, {
@@ -14386,7 +14712,7 @@ var IntrinsicFunctionResolver = class {
14386
14712
  const parameterName = parts.slice(1).join(":");
14387
14713
  if (!parameterName) throw new Error("Dynamic reference: ssm PARAMETER_NAME is required");
14388
14714
  this.logger.debug(`Resolving dynamic reference: ssm:${parameterName}`);
14389
- const client = getAwsClients().ssm;
14715
+ const client = this.clientsForRegion(this.explicitRegion).ssm;
14390
14716
  const command = new GetParameterCommand({
14391
14717
  Name: parameterName,
14392
14718
  WithDecryption: decrypt
@@ -15269,7 +15595,7 @@ var CloudControlProvider = class {
15269
15595
  if (context?.finalSnapshotIdentifier !== void 0) throw new ProvisioningError(`${logicalId} (${resourceType}) requires a final snapshot (DeletionPolicy: Snapshot), but the Cloud Control API delete route has no final-snapshot parameter. Re-run with --skip-final-snapshot after snapshotting manually, or retain the resource.`, resourceType, logicalId, physicalId);
15270
15596
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
15271
15597
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
15272
- const { ASGProvider } = await import("./asg-provider-iHWEv4gN.js").then((n) => n.n);
15598
+ const { ASGProvider } = await import("./asg-provider-C0L3_pHn.js").then((n) => n.n);
15273
15599
  return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
15274
15600
  }
15275
15601
  const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
@@ -22814,7 +23140,7 @@ const FLUSH_INTERVAL_MS = 2e3;
22814
23140
  const FLUSH_EVENT_THRESHOLD = 50;
22815
23141
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
22816
23142
  function getCdkdVersion() {
22817
- return "0.284.3";
23143
+ return "0.284.4";
22818
23144
  }
22819
23145
  /**
22820
23146
  * Generate a time-sortable unique run id, e.g.
@@ -25282,4 +25608,4 @@ var DeployEngine = class {
25282
25608
 
25283
25609
  //#endregion
25284
25610
  export { IntrinsicFunctionResolver as $, StackTerminationProtectionError as $n, buildDenyExternalAccessPolicy as $t, formatResourceLine as A, processStackMessages as An, withRetry as At, isExportAliasCollision as B, DependencyError as Bn, buildAssetRedirectMap as Bt, refusesFinalSnapshot as C, findLargeInlineResources as Cn, s3BucketDualStackDomainName as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, canonicalizeRegion as Dn, DiffCalculator as Dt, extractDeploymentEventError as E, PARTITION_TABLE as En, applyRoleArnIfSet as Et, red as F, resetAwsClients as Fn, rebuildClientForBucketRegion as Ft, clearOnUpdateRemoval as G, LockError as Gn, stripControlChars as Gt, stateKeySecretExposure as H, LocalInvokeBuildError as Hn, loadPublishableAssetManifest as Ht, yellow as I, setAwsClients as In, shouldRetainResource as It, findSilentDropProperties as J, PartialFailureError as Jn, ensureAssetStorage as Jt, ProviderRegistry as K, MissingCdkCliError as Kn, AssetModeResolver as Kt, collectDeclaredOutputNames as L, AssetError as Ln, AssetPublisher as Lt, cyan as M, resolveBucketRegion as Mn, TemplateParser as Mt, gray as N, AwsClients as Nn, LockManager as Nt, isStatefulRecreateTargetSync as O, derivePartitionAndUrlSuffix as On, INTRINSIC_KEYS as Ot, green as P, getAwsClients as Pn, S3StateBackend as Pt, isTerminationProtectionPropagationError as Q, StackHasActiveImportsError as Qn, validateContainerRepoName as Qt, collectPublishedOutputNames as R, CdkdError as Rn, stringifyValue as Rt, isFinalSnapshotError as S, MIGRATE_TMP_PREFIX as Sn, s3BucketDomainName as St, makeCanonicalizePropertiesFn as T, expectedOwnerParam as Tn, s3BucketWebsiteUrl as Tt, IAMRoleProvider as U, LocalMigrateError as Un, rewriteTemplateAssetReferences as Ut, secretBearingStateKeyWarning as V, DeployCancelledError as Vn, createAssetRedirectResolver as Vt, collectInlinePolicyNamesManagedBySiblings as W, LocalStartServiceError as Wn, escapeRegExp$1 as Wt, slowCcOperationTimeoutMs as X, ResourceTimeoutError as Xn, parseBootstrapMarker as Xt, CloudControlProvider as Y, ProvisioningError as Yn, getBootstrapMarkerKey as Yt, disableInstanceApiTermination as Z, ResourceUpdateNotSupportedError as Zn, validateAssetBucketName as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, resolveUseCdkBootstrapAssets as _n, TEMPLATE_SOURCED_RULES as _t, DeploymentEventsStore as a, AssetManifestLoader as an, withErrorHandling as ar, resolveExplicitPhysicalId as at, ccRoutedFinalSnapshotError as b, CFN_TEMPLATE_BODY_LIMIT as bn, scrubResourceRecord as bt, replayFailedOperations as c, synthesisStatusMessage as cn, isThrottlingError as cr, configBooleanRefusal as ct, updatePartialReason as d, resolveApp as dn, replayWarn as dt, buildDockerImage as en, StateError as er, cfnRefValueFromPhysicalId as et, UNSPECIFIED_SKIP_REASON as f, resolveAutoAssetStorage as fn, requireConfigArray as ft, computeImplicitDeleteEdges as g, resolveStateBucketWithDefaultAndSource as gn, STATE_SOURCED_READBACK_RULES as gt, IMPLICIT_DELETE_DEPENDENCIES as h, resolveStateBucketWithDefault as hn, STATE_SOURCED_CROSS_GENERATION_RULES as ht, DeploymentEventsReader as i, runDockerStreaming as in, normalizeAwsError as ir, normalizeAwsTagsToCfn as it, bold as j, clearBucketRegionCache as jn, DagBuilder as jt, renderStatefulReason as k, AssemblyReader as kn, describeTypeWithThrottleRetry as kt, replayRollback as l, getDefaultStateBucketName as ln, markNonRetryable as lr, configStringRefusal as lt, withResourceDeadline as m, resolveSkipPrefix as mn, requireConfigString as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, getDockerCmd as nn, formatError as nr, refStateLookupFromResource as nt, planFailedOps as o, getDockerImageBySourceHash as on, isMarkedNonRetryable as or, assertRegionMatch as ot, deleteSkipReason as p, resolveCaptureObservedState as pn, requireConfigObject as pt, findActionableSilentDrops as q, NestedStackChildDirectDestroyError as qn, BOOTSTRAP_MARKER_PREFIX as qt, DeployEngine as r, runDockerForeground as rn, isCdkdError as rr, WAFv2WebACLProvider as rt, planRollback as s, Synthesizer as sn, isRetryableTransientError as sr, coerceCfnBoolean as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, formatDockerLoginError as tn, SynthesisError as tr, getAccountInfo as tt, updatePartialMessage as u, getLegacyStateBucketName as un, __exportAll as ur, readConfigString as ut, PRE_DELETE_SNAPSHOT_TYPES as v, stateBucketExistenceConfirmed as vn, maskSecretsInText as vt, unsupportedFinalSnapshotError as w, uploadCfnTemplate as wn, s3BucketRegionalDomainName as wt, createPreDeleteFinalSnapshot as x, CFN_TEMPLATE_URL_LIMIT as xn, s3BucketArn as xt, buildFinalSnapshotIdentifier as y, warnDeprecatedNoPrefixCliFlag as yn, redactSecretsForState as yt, exportAliasCollisionScrubWarning as z, ConfigError as zn, WorkGraph as zt };
25285
- //# sourceMappingURL=deploy-engine-DJ-CgH80.js.map
25611
+ //# sourceMappingURL=deploy-engine-3Z7P1hKs.js.map