@go-to-k/cdkd 0.284.3 → 0.284.5

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:
@@ -10578,6 +10659,29 @@ function maskSecretsInText(text, secrets) {
10578
10659
  if (!regex) return text;
10579
10660
  return text.replace(regex, "***");
10580
10661
  }
10662
+ /**
10663
+ * Bind a {@link RecordedSecretValues} bag into a {@link SecretMasker} for a
10664
+ * caller to hand to a provider.
10665
+ *
10666
+ * The bag is captured BY REFERENCE and read on every call, and there is
10667
+ * deliberately NO `secrets.size === 0` short-circuit here: collapsing an empty
10668
+ * bag to the identity function at BIND time would go permanently blind to
10669
+ * everything added afterwards. {@link maskSecretsInText} makes that check at
10670
+ * CALL time, where it is correct and costs a `Map.size` read.
10671
+ *
10672
+ * Stated as a property rather than a live requirement, because it is worth
10673
+ * being exact about: every caller today FILLS its bag before binding — the
10674
+ * rollback executor's arms run `resolveReplayProps` first and only then build
10675
+ * the masker, and the deploy engine resolves before it calls the provider — so
10676
+ * a bind-time short-circuit would pass every existing integration. It is the
10677
+ * ORDER, not the reference capture, that makes them work now, and the order is
10678
+ * the kind of thing a later refactor reverses without noticing. The unit test
10679
+ * `masks values added to the bag AFTER the masker was built` is what holds the
10680
+ * property up on its own.
10681
+ */
10682
+ function createSecretMasker(secrets) {
10683
+ return (text) => maskSecretsInText(text, secrets);
10684
+ }
10581
10685
 
10582
10686
  //#endregion
10583
10687
  //#region src/provisioning/config-shape.ts
@@ -12283,6 +12387,44 @@ const MAX_DYNAMIC_REFERENCE_THROTTLE_RETRIES = 4;
12283
12387
  * without real waits (mirrors `describeTypeRetryDelays`).
12284
12388
  */
12285
12389
  const dynamicReferenceRetryDelays = {};
12390
+ /**
12391
+ * Is `region` safe to build an AWS SDK client from?
12392
+ *
12393
+ * This is a SECURITY gate, not an AWS region registry, and the distinction
12394
+ * decides how strict it is. The SDK turns a region into a hostname by
12395
+ * substitution — `https://ssm.{region}.amazonaws.com` — so a value carrying a
12396
+ * host delimiter escapes the label and re-points the endpoint: the measured
12397
+ * case is `evil.example.com#`, which yields
12398
+ * `https://ssm.evil.example.com/#.amazonaws.com` and sends a SigV4-SIGNED
12399
+ * request (access key id + signature) to an attacker-controlled host.
12400
+ *
12401
+ * The reachable input is `Fn::GetAZs`, whose argument is TEMPLATE-DERIVED and
12402
+ * can arrive through an `Fn::ImportValue` or a parameter — i.e. it is not
12403
+ * necessarily written by whoever runs the deploy. Before issue #1957 that value
12404
+ * only fed the `region-name` FILTER of a `DescribeAvailabilityZones` call and
12405
+ * never built a client, so binding lookups to a region is exactly what made it
12406
+ * reachable; the gate ships with the binding.
12407
+ *
12408
+ * So the predicate is CHARSET-based rather than shape-based: lowercase
12409
+ * alphanumerics and hyphens only, which cannot express `.`, `/`, `:`, `@`, `?`
12410
+ * or `#` and therefore cannot leave the hostname label. It deliberately does
12411
+ * NOT try to enumerate real regions — AWS keeps adding them
12412
+ * (`ap-southeast-7`, `il-central-1`, `mx-central-1`, `eusc-de-east-1`), and a
12413
+ * pattern tight enough to reject `----` would also reject the next one. A
12414
+ * region-shaped-but-nonexistent value is not a security problem: it resolves to
12415
+ * a hostname that does not exist and the SDK fails loudly.
12416
+ *
12417
+ * Note the sibling pattern in `src/cli/commands/state-file-keys.ts` is NOT
12418
+ * reusable here: it requires `^[a-z]{2}(-[a-z]+)+-\d+$`, which rejects
12419
+ * `eusc-de-east-1` (the European Sovereign Cloud partition's four-letter
12420
+ * prefix).
12421
+ *
12422
+ * Callers must {@link canonicalizeRegion} first — `US-EAST-1` is a documented
12423
+ * input and is lowercase-canonical, not invalid.
12424
+ */
12425
+ function isClientSafeRegion(region) {
12426
+ return /^[a-z0-9][a-z0-9-]{0,30}$/.test(region);
12427
+ }
12286
12428
  /** Test seam for {@link FABRICATED_ACCOUNT_INFO_TTL_MS} expiry. */
12287
12429
  const accountInfoClock = { now: () => Date.now() };
12288
12430
  let fabricatedAccountIdentity = null;
@@ -12421,6 +12563,63 @@ function stringifyParameterForLog(paramDef, value) {
12421
12563
  var IntrinsicFunctionResolver = class {
12422
12564
  logger = getLogger().child("IntrinsicFunctionResolver");
12423
12565
  resolverRegion;
12566
+ /**
12567
+ * The region the CONSTRUCTOR was given, or `undefined` when it was called
12568
+ * without one — unlike {@link resolverRegion}, which substitutes
12569
+ * `AWS_REGION` / `us-east-1` so every consumer has a string to work with.
12570
+ *
12571
+ * The distinction is load-bearing for {@link clientsForRegion} and for
12572
+ * nothing else: re-pointing an AWS lookup away from the ambient clients is
12573
+ * only safe when a caller SAID which region this resolver stands for.
12574
+ *
12575
+ * IN PRODUCTION THIS IS ALWAYS SET, and saying so matters more than the
12576
+ * guard it enables. Every construction site defaults the region BEFORE the
12577
+ * constructor and passes a `string` — `deploy.ts`, `scrub.ts`, `drift.ts`,
12578
+ * `import.ts`, `export.ts`, `diff-recursive.ts`, `rollback-executor.ts` —
12579
+ * so the `undefined` arm is reachable only through the no-argument
12580
+ * constructor, which nothing but tests uses. It is kept because the
12581
+ * parameter is optional and the arm must therefore exist, not because a
12582
+ * shipped path depends on it.
12583
+ *
12584
+ * That has a USER-VISIBLE consequence, deliberately accepted (issue #1957
12585
+ * review). For a REGION-AGNOSTIC stack (no `env.region`) run with neither
12586
+ * `--region` nor `AWS_REGION`, the region those callers compute is the
12587
+ * hard-coded `us-east-1` fallback, so the lookup now goes there — where
12588
+ * before it followed the ambient clients to whatever `~/.aws/config` said.
12589
+ * The new behaviour is the consistent one: `us-east-1` is already the region
12590
+ * cdkd keys that stack's state file, its lock and its export index under, so
12591
+ * the resolved value and the record that stores it now agree. Previously
12592
+ * they did not, which is the same class of defect this issue is about, one
12593
+ * layer up. A stack WITH an explicit `env.region` is unaffected — every
12594
+ * caller prefers it (`scrub.ts` does `stack.region || region`).
12595
+ */
12596
+ explicitRegion;
12597
+ /**
12598
+ * AWS clients pinned to a region OTHER than the ambient singleton's, built
12599
+ * lazily on first mismatch and keyed by region (issue #1957).
12600
+ *
12601
+ * Lifetime is deliberately the resolver's own, matching {@link cfnClients}
12602
+ * two fields down: both are per-region SDK clients this instance builds for
12603
+ * itself, and neither is destroyed, because `IntrinsicFunctionResolver` has
12604
+ * no teardown hook and every construction site (`DeployEngine`, `scrub`,
12605
+ * `drift`, `import`, `diff-recursive`, `export`, `rollback-executor`) would
12606
+ * have to grow one. The bound on what that costs is small and worth stating:
12607
+ * an entry exists only when a resolver's region DIFFERS from the ambient
12608
+ * one — i.e. only on a genuinely cross-region run — and at most one per
12609
+ * foreign region per resolver, versus the ambient clients which are already
12610
+ * created and destroyed per stack by `deploy.ts`.
12611
+ *
12612
+ * KEYED BY REGION ALONE, which means an entry also pins the CREDENTIAL
12613
+ * configuration of whichever ambient instance was current at the first
12614
+ * mismatch for that region. That is sound today because the credential half
12615
+ * is process-wide rather than per-stack: `--profile` comes from one CLI
12616
+ * option and `--role-arn` lands in `process.env`, so every ambient instance
12617
+ * a run installs carries the same one. Widen the key to include
12618
+ * {@link AwsClients.credentialConfig} the moment that stops being true —
12619
+ * per-stack credentials would otherwise let one stack's lookups run under
12620
+ * another's identity, which is a worse bug than the one this cache serves.
12621
+ */
12622
+ regionScopedClients = /* @__PURE__ */ new Map();
12424
12623
  strictGetAtt;
12425
12624
  cfnFallback;
12426
12625
  /**
@@ -12511,24 +12710,17 @@ var IntrinsicFunctionResolver = class {
12511
12710
  * nested stacks ship or that pass starts recording secrets; a resolver per
12512
12711
  * node is the fix then, not a wider key here.
12513
12712
  *
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.
12713
+ * The OTHER half of the same outcome the lookups themselves reading the
12714
+ * process-ambient `getAwsClients()` singleton, whose region is whichever the
12715
+ * process installed last was issue
12716
+ * [#1957](https://github.com/go-to-k/cdkd/issues/1957) and is now closed by
12717
+ * {@link clientsForRegion}: a resolver whose region differs from the ambient
12718
+ * one builds its own region-pinned clients (carrying the ambient profile /
12719
+ * credentials) instead of reading whatever the singleton currently holds.
12720
+ * The two halves remain SEPARATE mechanisms and both are needed — this field
12721
+ * stops a resolved value from travelling between regions or stacks, while
12722
+ * the scoped clients stop the FIRST resolution from reading the wrong region
12723
+ * (no cache involved, so nothing here could ever have prevented it).
12532
12724
  */
12533
12725
  cachedDynamicReferences = /* @__PURE__ */ new Map();
12534
12726
  /**
@@ -12554,6 +12746,7 @@ var IntrinsicFunctionResolver = class {
12554
12746
  warnedUnrecognizedSsmTypes = /* @__PURE__ */ new Set();
12555
12747
  constructor(region, options) {
12556
12748
  this.resolverRegion = region || process.env["AWS_REGION"] || "us-east-1";
12749
+ this.explicitRegion = region || void 0;
12557
12750
  this.strictGetAtt = options?.strictGetAtt ?? false;
12558
12751
  this.cfnFallback = options?.cfnFallback ?? true;
12559
12752
  }
@@ -12566,6 +12759,133 @@ var IntrinsicFunctionResolver = class {
12566
12759
  this.physicalIdFallbackCount = 0;
12567
12760
  }
12568
12761
  /**
12762
+ * AWS clients for a REGION-SENSITIVE lookup, pinned to `targetRegion`
12763
+ * (issue [#1957](https://github.com/go-to-k/cdkd/issues/1957)).
12764
+ *
12765
+ * Every lookup in this class used to read `getAwsClients()` — the
12766
+ * PROCESS-GLOBAL singleton, whose region is whichever one the process
12767
+ * installed last. That is not the same thing as the region this resolver
12768
+ * stands for, and the gap is reachable on main:
12769
+ *
12770
+ * - `cdkd deploy` defaults to `--stack-concurrency 4` and re-points the
12771
+ * singleton per stack, so two stacks in different regions race for one
12772
+ * mutable global and stack B's `GetSecretValue` / `GetParameter` can run
12773
+ * against stack A's client. The resolved value is redacted on its way into
12774
+ * state, so nothing downstream records which region answered.
12775
+ * - `cdkd scrub --all` installs the clients ONCE while resolving per-stack
12776
+ * regions, so a region-B `SecureString` whose region-A namesake is a plain
12777
+ * `String` is classified PUBLIC and left in PLAINTEXT in state.json — the
12778
+ * same disclosure class as GHSA-p5qg-v9gv-hc7w, not merely a wrong value.
12779
+ * - `cdkd drift --revert` WRITES the resolved value to a live resource, so
12780
+ * there the wrong region is a wrong write rather than a wrong report.
12781
+ *
12782
+ * Fixing it here rather than at the ~10 `setAwsClients` call sites is what
12783
+ * makes it one mechanism instead of a per-command patch: this class already
12784
+ * knows its own region, and every construction site already passes the
12785
+ * per-stack one.
12786
+ *
12787
+ * REUSING THE AMBIENT CLIENTS REQUIRES PROOF THAT THEY ALREADY POINT AT
12788
+ * `targetRegion`, and the direction of that test is the whole correctness
12789
+ * argument. CloudFormation semantics say a stack's dynamic references resolve
12790
+ * in the STACK's region, and every construction site passes exactly that — so
12791
+ * once a region has been named, sending the lookup there is not an
12792
+ * optimisation to be justified, it is the requirement. Whether the ambient
12793
+ * singleton happens to agree only decides whether an object allocation can be
12794
+ * skipped.
12795
+ *
12796
+ * An earlier revision had this backwards twice over, and both failures are
12797
+ * worth naming because each looks reasonable in isolation.
12798
+ *
12799
+ * It first declined to override whenever the ambient region was UNKNOWN,
12800
+ * reasoning that overriding on an unproven mismatch might re-point a lookup
12801
+ * that works today. That fails OPEN, on the COMMON configuration: `aws
12802
+ * configure` writes the region to `~/.aws/config`, and `cdkd scrub` sets a
12803
+ * client region only when `--region` is passed. The disclosure this issue
12804
+ * exists to close stayed reachable — profile region `us-east-1`, stack B in
12805
+ * `ap-northeast-1`, a name that is `String` in A and `SecureString` in B,
12806
+ * `cdkd scrub --all` with no flags: B's reference answered by A, classified
12807
+ * public, plaintext left in `state.json`.
12808
+ *
12809
+ * It then determined the ambient region by reading `process.env` here, which
12810
+ * is worse than not knowing: the SDK memoizes a region-less client's region
12811
+ * at its first resolution while `deploy.ts`'s `switchRegion` keeps mutating
12812
+ * `AWS_REGION` per stack and restores it in each stack's `finally`, so the
12813
+ * environment could say `baseRegion` for a client long since pinned
12814
+ * elsewhere — and this method would conclude MATCH and hand back clients
12815
+ * pointing somewhere else.
12816
+ *
12817
+ * The fix for THAT was to ask the SDK (`ssm.config.region()`), and it was
12818
+ * still wrong, in a way worth writing down because it looks airtight. An
12819
+ * unconfigured `AwsClients` is not a bag of clients, it is a bag of DEFERRED
12820
+ * client constructions: `clientOptions` omits `region`, the getters are lazy,
12821
+ * and each member therefore samples the mutating environment at its own
12822
+ * instant and memoizes a possibly DIFFERENT region. Asking `ssm` measures one
12823
+ * member and says nothing about `secretsManager`, so the seam could short-
12824
+ * circuit on a us-west-2 `ssm` and then hand out a bag whose `secretsManager`
12825
+ * pins us-east-1 a moment later — issue #1957's Site 1 surviving inside the
12826
+ * arm meant to fix it.
12827
+ *
12828
+ * So the short-circuit is taken ONLY when the ambient's region is
12829
+ * CONFIGURED. That is not a heuristic: a configured bag passes `region` to
12830
+ * every member ({@link AwsClients.clientOptions}), so its members agree by
12831
+ * construction, and {@link AwsClients.withRegion} always sets one, so every
12832
+ * derived bag is internally consistent too. An unconfigured ambient is not
12833
+ * "of unknown region", it is "of not-yet-decided region", and there is
12834
+ * nothing to compare against — so it SCOPES. That is the same "unknown means
12835
+ * SCOPE, not skip" rule as above, applied one level deeper.
12836
+ *
12837
+ * Three arms return the ambient instance, each for a reason that is not
12838
+ * "we could not prove a mismatch":
12839
+ *
12840
+ * 1. No `targetRegion` — no region was ever named (see
12841
+ * {@link explicitRegion}), so there is nothing to bind to.
12842
+ * 2. `targetRegion` is not safe to build a client from (see
12843
+ * {@link isClientSafeRegion}) — which THROWS. An earlier revision warned
12844
+ * and fell back to the ambient clients, reasoning that a malformed region
12845
+ * reaching here is a cdkd bug and failing every lookup would turn it into
12846
+ * an outage. That put this arm on the wrong side of the two-severity
12847
+ * design: falling back to the ambient means READING ANOTHER REGION, which
12848
+ * for `scrub` / `drift` / `import` — whose region is state-derived — is
12849
+ * the disclosure this issue exists to close (a region-B `SecureString`
12850
+ * classified against a region-A `String`). A stopped command is strictly
12851
+ * better than a silent wrong-region read. The `Fn::GetAZs` entry still
12852
+ * validates EARLIER so it can give a message naming the template
12853
+ * construct; this arm is the backstop that guarantees no call site,
12854
+ * present or future, routes unvalidated input into an SDK endpoint.
12855
+ * 3. The installed clients cannot DERIVE a sibling — `withRegion` is absent.
12856
+ * In production that never happens: `getAwsClients()` returns an
12857
+ * `AwsClients`. It is true only of a test double, and it is checked
12858
+ * EXPLICITLY rather than left to emerge, for a reason the review of this
12859
+ * change made concrete. The ~260 suites that stub `getAwsClients()` with a
12860
+ * plain object used to stay on the ambient path as a side effect of the
12861
+ * `undefined`-region guard above — the very guard that made the disclosure
12862
+ * reachable. Removing that guard without putting something deliberate in
12863
+ * its place would have traded a security hole for ~260 `TypeError`s, so
12864
+ * the test-double case is now its own named arm and the security arm no
12865
+ * longer has a testing job to do. Suites that are ABOUT region scoping use
12866
+ * a real `AwsClients` and are unaffected by it.
12867
+ *
12868
+ * Regions are canonicalised on both sides before comparing, because
12869
+ * `--region US-EAST-1` is a documented input and the repo lowercases
12870
+ * elsewhere (`canonicalizeRegion`, issues #1795 / #1850). Without it an
12871
+ * uppercase spelling would build a second client for the same physical
12872
+ * region — benign, but wasteful and confusing in a debug log.
12873
+ */
12874
+ clientsForRegion(targetRegion) {
12875
+ const ambient = getAwsClients();
12876
+ if (!targetRegion) return ambient;
12877
+ const target = canonicalizeRegion(targetRegion);
12878
+ 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.`);
12879
+ if (typeof ambient.withRegion !== "function") return ambient;
12880
+ const cached = this.regionScopedClients.get(target);
12881
+ if (cached) return cached;
12882
+ if (canonicalizeRegion(ambient.configuredRegion) === target) return ambient;
12883
+ const scoped = ambient.withRegion(target);
12884
+ this.regionScopedClients.set(target, scoped);
12885
+ this.logger.debug(`Using region-scoped AWS clients for ${target}`);
12886
+ return scoped;
12887
+ }
12888
+ /**
12569
12889
  * Resolve parameter values from template Parameters section
12570
12890
  *
12571
12891
  * Merges default values from template with user-provided parameter values.
@@ -12617,7 +12937,7 @@ var IntrinsicFunctionResolver = class {
12617
12937
  * Used for parameters with type AWS::SSM::Parameter::Value<...>.
12618
12938
  */
12619
12939
  async resolveSSMParameter(parameterName) {
12620
- return (await getAwsClients().ssm.send(new GetParameterCommand({ Name: parameterName }))).Parameter?.Value ?? "";
12940
+ return (await this.clientsForRegion(this.explicitRegion).ssm.send(new GetParameterCommand({ Name: parameterName }))).Parameter?.Value ?? "";
12621
12941
  }
12622
12942
  /**
12623
12943
  * Coerce parameter value to the correct type based on parameter definition
@@ -13190,7 +13510,7 @@ var IntrinsicFunctionResolver = class {
13190
13510
  const cached = cachedEc2InstanceAttributes[cacheKey];
13191
13511
  if (cached !== void 0) return cached;
13192
13512
  try {
13193
- const instance = (await getAwsClients().ec2.send(new DescribeInstancesCommand({ InstanceIds: [physicalId] }))).Reservations?.[0]?.Instances?.[0];
13513
+ const instance = (await this.clientsForRegion(this.explicitRegion).ec2.send(new DescribeInstancesCommand({ InstanceIds: [physicalId] }))).Reservations?.[0]?.Instances?.[0];
13194
13514
  let value;
13195
13515
  switch (attributeName) {
13196
13516
  case "PrivateIp":
@@ -13224,7 +13544,7 @@ var IntrinsicFunctionResolver = class {
13224
13544
  if (resourceType === "AWS::EC2::LaunchTemplate") {
13225
13545
  if (attributeName === "LatestVersionNumber" || attributeName === "DefaultVersionNumber") {
13226
13546
  try {
13227
- const lt = (await getAwsClients().ec2.send(new DescribeLaunchTemplatesCommand({ LaunchTemplateIds: [physicalId] }))).LaunchTemplates?.[0];
13547
+ const lt = (await this.clientsForRegion(this.explicitRegion).ec2.send(new DescribeLaunchTemplatesCommand({ LaunchTemplateIds: [physicalId] }))).LaunchTemplates?.[0];
13228
13548
  const value = attributeName === "LatestVersionNumber" ? lt?.LatestVersionNumber : lt?.DefaultVersionNumber;
13229
13549
  if (value !== void 0 && value !== null) return String(value);
13230
13550
  } catch (err) {
@@ -13873,7 +14193,9 @@ var IntrinsicFunctionResolver = class {
13873
14193
  if ("Region" in args && args["Region"] !== void 0 && args["Region"] !== null) {
13874
14194
  const resolvedRegion = await this.resolveValue(args["Region"], context);
13875
14195
  if (typeof resolvedRegion !== "string" || resolvedRegion === "") throw new Error(`Fn::GetStackOutput: Region must resolve to a non-empty string, got ${typeof resolvedRegion}`);
13876
- region = resolvedRegion;
14196
+ const requestedRegion = canonicalizeRegion(resolvedRegion);
14197
+ 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.`);
14198
+ region = requestedRegion;
13877
14199
  }
13878
14200
  let roleArn;
13879
14201
  if ("RoleArn" in args && args["RoleArn"] !== void 0 && args["RoleArn"] !== null) {
@@ -14056,28 +14378,52 @@ var IntrinsicFunctionResolver = class {
14056
14378
  async resolveGetAZs(value, context) {
14057
14379
  const resolvedValue = await this.resolveValue(value, context);
14058
14380
  let region;
14059
- if (typeof resolvedValue === "string" && resolvedValue !== "") region = resolvedValue;
14060
- else region = (await getAccountInfo(this.resolverRegion)).region;
14381
+ /**
14382
+ * Which region's clients answer the `DescribeAvailabilityZones` below.
14383
+ *
14384
+ * `DescribeAvailabilityZones` lists the AZs of the region the CLIENT is
14385
+ * pointed at; the `region-name` filter narrows that listing, it does not
14386
+ * widen it to another region. So a foreign-region client returns an EMPTY
14387
+ * list, which this method then caches and hands back as the resolved value
14388
+ * of `Fn::GetAZs` — silently, since an empty list is not an error here
14389
+ * (issue #1957).
14390
+ *
14391
+ * When the template names a region explicitly, THAT is the region to talk
14392
+ * to. Otherwise fall back to the resolver's own — but only when it was
14393
+ * given explicitly (see {@link explicitRegion}).
14394
+ */
14395
+ let clientRegion;
14396
+ if (typeof resolvedValue === "string" && resolvedValue !== "") {
14397
+ const requested = canonicalizeRegion(resolvedValue);
14398
+ 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.`);
14399
+ region = requested;
14400
+ clientRegion = requested;
14401
+ } else {
14402
+ region = (await getAccountInfo(this.resolverRegion)).region;
14403
+ clientRegion = this.explicitRegion;
14404
+ }
14061
14405
  const cached = cachedAvailabilityZones[region];
14062
14406
  if (cached) {
14063
14407
  this.logger.debug(`Resolved Fn::GetAZs from cache: ${region} -> ${JSON.stringify(cached)}`);
14064
14408
  return cached;
14065
14409
  }
14066
- const ec2Client = getAwsClients().ec2;
14410
+ const ec2Client = this.clientsForRegion(clientRegion).ec2;
14411
+ let azNames;
14067
14412
  try {
14068
- const azNames = ((await ec2Client.send(new DescribeAvailabilityZonesCommand({ Filters: [{
14413
+ azNames = ((await ec2Client.send(new DescribeAvailabilityZonesCommand({ Filters: [{
14069
14414
  Name: "region-name",
14070
14415
  Values: [region]
14071
14416
  }, {
14072
14417
  Name: "state",
14073
14418
  Values: ["available"]
14074
14419
  }] }))).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
14420
  } catch (error) {
14079
14421
  throw new Error(`Fn::GetAZs: failed to describe availability zones for region '${region}': ${error instanceof Error ? error.message : String(error)}`);
14080
14422
  }
14423
+ 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.`);
14424
+ cachedAvailabilityZones[region] = azNames;
14425
+ this.logger.debug(`Resolved Fn::GetAZs: ${region} -> ${JSON.stringify(azNames)}`);
14426
+ return azNames;
14081
14427
  }
14082
14428
  /**
14083
14429
  * Resolve pseudo parameters
@@ -14226,7 +14572,7 @@ var IntrinsicFunctionResolver = class {
14226
14572
  if (!versionStage) versionStage = "AWSCURRENT";
14227
14573
  if (!secretId) throw new Error("Dynamic reference: secretsmanager SECRET_ID is required");
14228
14574
  this.logger.debug(`Resolving dynamic reference: secretsmanager:${secretId}:SecretString:${jsonKey}:${versionStage}:${versionId}`);
14229
- const client = getAwsClients().secretsManager;
14575
+ const client = this.clientsForRegion(this.explicitRegion).secretsManager;
14230
14576
  const command = new GetSecretValueCommand({
14231
14577
  SecretId: secretId,
14232
14578
  ...versionStage && versionStage !== "" && { VersionStage: versionStage },
@@ -14354,10 +14700,13 @@ var IntrinsicFunctionResolver = class {
14354
14700
  * - It says NOTHING about concurrency. The client is captured before the
14355
14701
  * first attempt, so a sibling stack's teardown (`stackAwsClients.destroy()`
14356
14702
  * 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.
14703
+ * failure on the next attempt, and the retry does not make that safe. Issue
14704
+ * [#1957](https://github.com/go-to-k/cdkd/issues/1957) NARROWED this rather
14705
+ * than removing it: a lookup whose region differs from the ambient one now
14706
+ * runs on {@link clientsForRegion}'s own clients, which no sibling stack can
14707
+ * destroy because nothing else in the process holds a reference to them. A
14708
+ * sibling in the SAME region still shares the ambient instance, so the
14709
+ * window survives exactly where the two stacks agree on the region.
14361
14710
  */
14362
14711
  sendWithThrottleRetry(operation, label) {
14363
14712
  return withRetry(operation, label, {
@@ -14386,7 +14735,7 @@ var IntrinsicFunctionResolver = class {
14386
14735
  const parameterName = parts.slice(1).join(":");
14387
14736
  if (!parameterName) throw new Error("Dynamic reference: ssm PARAMETER_NAME is required");
14388
14737
  this.logger.debug(`Resolving dynamic reference: ssm:${parameterName}`);
14389
- const client = getAwsClients().ssm;
14738
+ const client = this.clientsForRegion(this.explicitRegion).ssm;
14390
14739
  const command = new GetParameterCommand({
14391
14740
  Name: parameterName,
14392
14741
  WithDecryption: decrypt
@@ -15269,7 +15618,7 @@ var CloudControlProvider = class {
15269
15618
  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
15619
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
15271
15620
  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);
15621
+ const { ASGProvider } = await import("./asg-provider-C1NtROYt.js").then((n) => n.n);
15273
15622
  return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
15274
15623
  }
15275
15624
  const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
@@ -21875,21 +22224,52 @@ const SKIP_FINAL_SNAPSHOT_FLAG = "--skip-final-snapshot";
21875
22224
  * engine's five sites (CREATE, the property-driven replacement, the
21876
22225
  * `--recreate-via-*` destroy-then-create, the `--replace` delete-first
21877
22226
  * fallback, and the update-failure replacement) are all driven by freshly
21878
- * resolved TEMPLATE properties, so they deliberately pass no context and the
21879
- * refusal stands where the user can edit the input.
22227
+ * resolved TEMPLATE properties, so they never set THIS FLAG and the refusal
22228
+ * stands where the user can edit the input. They DO pass a context — since
22229
+ * issue #1932 every create site REACHED FROM THE ENGINE carries a
22230
+ * `maskSecrets` capability — so the invariant is "no `replayingState`", not
22231
+ * "no context object". (A provider that re-creates inside its own `update()`
22232
+ * still passes none; see `CreateContext`.)
21880
22233
  *
21881
22234
  * The remaining call sites are the providers that re-create inside their own
21882
22235
  * `update()` (`this.create(...)` in ACM certificate / IAM managed policy / IAM
21883
22236
  * role / Lambda permission / SNS subscription). Those are NOT template-driven
21884
22237
  * — this executor's `revert` arm calls `provider.update(...)` with
21885
22238
  * `previousState.properties`, so they forward a STATE record on a replay — but
21886
- * they CANNOT receive a context, because `update()` has no context parameter.
22239
+ * they CANNOT receive a `CreateContext`: `update()`'s own context is an
22240
+ * `UpdateContext`, which carries no `replayingState` to forward.
21887
22241
  * The constraint that follows is on providers, not on this constant: a
21888
22242
  * provider with a create-side pre-flight refusal must not re-create inside
21889
22243
  * `update()`. See `CreateContext` in `src/types/resource.ts`.
21890
22244
  */
21891
22245
  const REPLAYING_STATE_CREATE_CONTEXT = { replayingState: true };
21892
22246
  /**
22247
+ * The rollback arms' {@link CreateContext}, with this op's secret masker bound
22248
+ * in (issue #1932 item 3).
22249
+ *
22250
+ * The rollback path needs this MORE than the forward deploy does, not less:
22251
+ * {@link resolveReplayProps} deliberately re-resolves every redacted
22252
+ * `{{resolve:...}}` expression back to plaintext before handing the bag to a
22253
+ * provider, so a replayed bag is guaranteed to carry the concrete secret
22254
+ * whenever the resource has one. Leaving the masker off here would have left
22255
+ * the contract applied at one caller and absent at the one whose bag is
22256
+ * provably plaintext.
22257
+ *
22258
+ * Spreads the shared constant rather than mutating it: `maskSecrets` is
22259
+ * per-op, and a module-level object is shared by every op in the run.
22260
+ *
22261
+ * Called AFTER `resolveReplayProps` has filled `secrets` at every call site, so
22262
+ * the masker sees this op's re-resolved values. `createSecretMasker` reads the
22263
+ * bag by reference on every call and so does not depend on that ordering, but
22264
+ * the ordering is what makes it correct here without relying on that.
22265
+ */
22266
+ function replayingStateCreateContext(secrets) {
22267
+ return {
22268
+ ...REPLAYING_STATE_CREATE_CONTEXT,
22269
+ maskSecrets: createSecretMasker(secrets)
22270
+ };
22271
+ }
22272
+ /**
21893
22273
  * Which provisioning layer a delete must be judged against: the CURRENT
21894
22274
  * state record wins (it is what state says AWS holds right now), with the
21895
22275
  * journaled op's routing as the legacy-state fallback. Shared by both
@@ -22444,7 +22824,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
22444
22824
  let deletedNewFirst = false;
22445
22825
  let createResult;
22446
22826
  try {
22447
- createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps }, REPLAYING_STATE_CREATE_CONTEXT), op.logicalId, {
22827
+ createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps }, replayingStateCreateContext(secrets)), op.logicalId, {
22448
22828
  ...RECREATE_RETRY_SCHEDULE,
22449
22829
  logger,
22450
22830
  ...isInterrupted && {
@@ -22467,7 +22847,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
22467
22847
  delete stateResources[op.logicalId];
22468
22848
  await afterOp?.(op.logicalId);
22469
22849
  try {
22470
- createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps }, REPLAYING_STATE_CREATE_CONTEXT), op.logicalId, {
22850
+ createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps }, replayingStateCreateContext(secrets)), op.logicalId, {
22471
22851
  ...RECREATE_RETRY_SCHEDULE,
22472
22852
  logger,
22473
22853
  ...isInterrupted && {
@@ -22540,7 +22920,8 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
22540
22920
  current.physicalId,
22541
22921
  op.resourceType,
22542
22922
  desiredProps ?? {},
22543
- currentProps ?? {}
22923
+ currentProps ?? {},
22924
+ { maskSecrets: createSecretMasker(secrets) }
22544
22925
  ], op.logicalId, logger, isInterrupted);
22545
22926
  stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(previousState, revertResult), secrets, previousState.properties);
22546
22927
  const rollbackPartial = updatePartialReason(revertResult);
@@ -22683,7 +23064,8 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
22683
23064
  current.physicalId,
22684
23065
  op.resourceType,
22685
23066
  desiredProps ?? {},
22686
- attemptedProps ?? {}
23067
+ attemptedProps ?? {},
23068
+ { maskSecrets: createSecretMasker(secrets) }
22687
23069
  ], op.logicalId, logger, options.isInterrupted);
22688
23070
  stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(prev, revertFailedResult), secrets, prev.properties);
22689
23071
  const revertFailedPartial = updatePartialReason(revertFailedResult);
@@ -22814,7 +23196,7 @@ const FLUSH_INTERVAL_MS = 2e3;
22814
23196
  const FLUSH_EVENT_THRESHOLD = 50;
22815
23197
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
22816
23198
  function getCdkdVersion() {
22817
- return "0.284.3";
23199
+ return "0.284.5";
22818
23200
  }
22819
23201
  /**
22820
23202
  * Generate a time-sortable unique run id, e.g.
@@ -24582,7 +24964,7 @@ var DeployEngine = class {
24582
24964
  * #960 follow-up) and the name-idempotent same-id guard (issue #1238) so
24583
24965
  * the two --replace escape hatches cannot drift apart.
24584
24966
  */
24585
- async replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateReplacePolicy) {
24967
+ async replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, createContext, updateReplacePolicy) {
24586
24968
  const finalSnapshotIdentifier = await this.prepareFinalSnapshotForDelete(logicalId, resourceType, currentResource, updateReplacePolicy);
24587
24969
  let deleteResult;
24588
24970
  try {
@@ -24599,7 +24981,7 @@ var DeployEngine = class {
24599
24981
  this.logger.info(` ${green("✓")} Old resource deleted`);
24600
24982
  this.logger.info(` Re-creating ${logicalId}...`);
24601
24983
  try {
24602
- return await withRetry(() => this.withRetry(() => replaceProvider.create(logicalId, resourceType, replaceProps), logicalId, void 0, void 0, replaceProvider), logicalId, {
24984
+ return await withRetry(() => this.withRetry(() => replaceProvider.create(logicalId, resourceType, replaceProps, createContext), logicalId, void 0, void 0, replaceProvider), logicalId, {
24603
24985
  maxRetries: 8,
24604
24986
  initialDelayMs: 2e3,
24605
24987
  maxDelayMs: 1e4,
@@ -24633,6 +25015,7 @@ var DeployEngine = class {
24633
25015
  }, stackName);
24634
25016
  const resolvedProps = await this.resolver.resolve(desiredProps, context);
24635
25017
  this.perResourceTemplateProps.set(logicalId, desiredProps);
25018
+ const createSecrets = context.recordedSecretValues ?? /* @__PURE__ */ new Map();
24636
25019
  if (context.recordedSecretValues) this.perResourceSecrets.set(logicalId, context.recordedSecretValues);
24637
25020
  this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
24638
25021
  this.attemptedResolvedProps.set(logicalId, resolvedProps);
@@ -24642,7 +25025,7 @@ var DeployEngine = class {
24642
25025
  });
24643
25026
  const createProvider = createDecision.provider;
24644
25027
  const createProps = createDecision.provisionedBy === "cc-api" ? this.preparePropertiesForCcApi(resourceType, resolvedProps, logicalId) : resolvedProps;
24645
- const result = await this.withRetry(() => createProvider.create(logicalId, resourceType, createProps), logicalId, void 0, void 0, createProvider);
25028
+ const result = await this.withRetry(() => createProvider.create(logicalId, resourceType, createProps, { maskSecrets: createSecretMasker(createSecrets) }), logicalId, void 0, void 0, createProvider);
24646
25029
  const dependencies = this.extractAllDependencies(template, logicalId);
24647
25030
  const templateAttrs = this.extractTemplateAttributes(template, logicalId);
24648
25031
  stateResources[logicalId] = {
@@ -24753,7 +25136,7 @@ var DeployEngine = class {
24753
25136
  this.logger.info(` ${green("✓")} Old resource deleted`);
24754
25137
  }
24755
25138
  this.logger.info(` Creating new ${logicalId}...`);
24756
- createResult = await withRetry(() => this.withRetry(() => replaceProvider.create(logicalId, resourceType, replaceProps), logicalId, void 0, void 0, replaceProvider), logicalId, {
25139
+ createResult = await withRetry(() => this.withRetry(() => replaceProvider.create(logicalId, resourceType, replaceProps, { maskSecrets: createSecretMasker(updateSecrets) }), logicalId, void 0, void 0, replaceProvider), logicalId, {
24757
25140
  maxRetries: 8,
24758
25141
  initialDelayMs: 2e3,
24759
25142
  maxDelayMs: 1e4,
@@ -24767,7 +25150,7 @@ var DeployEngine = class {
24767
25150
  this.logger.info(` Creating new ${logicalId}...`);
24768
25151
  let deletedOldFirst = false;
24769
25152
  try {
24770
- createResult = await this.withRetry(() => replaceProvider.create(logicalId, resourceType, replaceProps), logicalId, void 0, void 0, replaceProvider);
25153
+ createResult = await this.withRetry(() => replaceProvider.create(logicalId, resourceType, replaceProps, { maskSecrets: createSecretMasker(updateSecrets) }), logicalId, void 0, void 0, replaceProvider);
24771
25154
  } catch (createError) {
24772
25155
  const createMsg = createError instanceof Error ? createError.message : String(createError);
24773
25156
  if (!isNameCollisionError(createMsg)) throw createError;
@@ -24776,7 +25159,7 @@ var DeployEngine = class {
24776
25159
  if (this.options.replace !== true) throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but the create-first attempt collided with the existing resource: ${createMsg}. ${nameOrigin.descriptor}, so the CloudFormation-style safe replacement order (create the new resource before deleting the old) cannot reuse the occupied name — CloudFormation refuses this shape with "cannot update a stack when a custom-named resource requires replacing". ${nameOrigin.remedy}, or re-run with \`cdkd deploy --replace\` to delete the old resource FIRST and recreate it under the same name (the resource is briefly unavailable while it is recreated).`, "NAMED_REPLACEMENT_COLLISION");
24777
25160
  this.logger.info(` Create-first collided with the existing resource's name and --replace is set — deleting old ${logicalId} (${currentResource.physicalId}) first...`);
24778
25161
  deletedOldFirst = true;
24779
- createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateReplacePolicy);
25162
+ createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, { maskSecrets: createSecretMasker(updateSecrets) }, updateReplacePolicy);
24780
25163
  }
24781
25164
  if (!deletedOldFirst && createResult.physicalId === currentResource.physicalId) {
24782
25165
  const idempotentNameOrigin = this.replacementNameOrigin(logicalId, currentResource.physicalId);
@@ -24784,7 +25167,7 @@ var DeployEngine = class {
24784
25167
  if (this.options.replace !== true) throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but its Create API is name-idempotent: the create-first attempt returned the EXISTING resource (${currentResource.physicalId}) instead of creating a new one, so deleting the "old" resource would silently destroy the resource the deploy just reported as created. ${idempotentNameOrigin.descriptor}; ${idempotentNameOrigin.remedy}, or re-run with \`cdkd deploy --replace\` to delete the old resource FIRST and recreate it under the same name (the resource is briefly unavailable while it is recreated). Note: this branch is also reached when the old resource was deleted out-of-band and the physical id is name-derived — there the create was a genuine fresh create; \`--replace\` converges that case too.`, "NAMED_REPLACEMENT_IDEMPOTENT_CREATE");
24785
25168
  this.logger.info(` Create-first returned the existing resource (name-idempotent Create API) and --replace is set — deleting old ${logicalId} (${currentResource.physicalId}) first...`);
24786
25169
  deletedOldFirst = true;
24787
- createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateReplacePolicy);
25170
+ createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, { maskSecrets: createSecretMasker(updateSecrets) }, updateReplacePolicy);
24788
25171
  }
24789
25172
  if (deletedOldFirst) {} else if (updateReplacePolicy === "Retain") this.logger.info(` Retaining old ${logicalId} (${currentResource.physicalId}) - UpdateReplacePolicy: Retain`);
24790
25173
  else {
@@ -24844,7 +25227,7 @@ var DeployEngine = class {
24844
25227
  let result;
24845
25228
  let resultProvisionedBy = updateDecision.provisionedBy;
24846
25229
  try {
24847
- result = await this.withRetry(() => updateProvider.update(logicalId, currentResource.physicalId, resourceType, updateProps, currentProps), logicalId, void 0, void 0, updateProvider);
25230
+ result = await this.withRetry(() => updateProvider.update(logicalId, currentResource.physicalId, resourceType, updateProps, currentProps, { maskSecrets: createSecretMasker(updateSecrets) }), logicalId, void 0, void 0, updateProvider);
24848
25231
  } catch (updateError) {
24849
25232
  const msg = updateError instanceof Error ? updateError.message : String(updateError);
24850
25233
  const ccUnsupported = msg.includes("UnsupportedActionException") || msg.includes("does not support UPDATE");
@@ -24876,7 +25259,7 @@ var DeployEngine = class {
24876
25259
  });
24877
25260
  const replProvider = replDecision.provider;
24878
25261
  const replProps = replDecision.provisionedBy === "cc-api" ? this.preparePropertiesForCcApi(resourceType, resolvedProps, logicalId) : resolvedProps;
24879
- const createResult = await this.withRetry(() => replProvider.create(logicalId, resourceType, replProps), logicalId, void 0, void 0, replProvider);
25262
+ const createResult = await this.withRetry(() => replProvider.create(logicalId, resourceType, replProps, { maskSecrets: createSecretMasker(updateSecrets) }), logicalId, void 0, void 0, replProvider);
24880
25263
  const replacementResult = {
24881
25264
  physicalId: createResult.physicalId,
24882
25265
  wasReplaced: true,
@@ -25281,5 +25664,5 @@ var DeployEngine = class {
25281
25664
  };
25282
25665
 
25283
25666
  //#endregion
25284
- 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
25667
+ export { IntrinsicFunctionResolver as $, StackHasActiveImportsError as $n, validateContainerRepoName as $t, formatResourceLine as A, AssemblyReader as An, describeTypeWithThrottleRetry as At, isExportAliasCollision as B, ConfigError as Bn, WorkGraph as Bt, refusesFinalSnapshot as C, MIGRATE_TMP_PREFIX as Cn, s3BucketDomainName as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, PARTITION_TABLE as Dn, applyRoleArnIfSet as Dt, extractDeploymentEventError as E, expectedOwnerParam as En, s3BucketWebsiteUrl as Et, red as F, getAwsClients as Fn, S3StateBackend as Ft, clearOnUpdateRemoval as G, LocalStartServiceError as Gn, escapeRegExp$1 as Gt, stateKeySecretExposure as H, DeployCancelledError as Hn, createAssetRedirectResolver as Ht, yellow as I, resetAwsClients as In, rebuildClientForBucketRegion as It, findSilentDropProperties as J, NestedStackChildDirectDestroyError as Jn, BOOTSTRAP_MARKER_PREFIX as Jt, ProviderRegistry as K, LockError as Kn, stripControlChars as Kt, collectDeclaredOutputNames as L, setAwsClients as Ln, shouldRetainResource as Lt, cyan as M, clearBucketRegionCache as Mn, DagBuilder as Mt, gray as N, resolveBucketRegion as Nn, TemplateParser as Nt, isStatefulRecreateTargetSync as O, canonicalizeRegion as On, DiffCalculator as Ot, green as P, AwsClients as Pn, LockManager as Pt, isTerminationProtectionPropagationError as Q, ResourceUpdateNotSupportedError as Qn, validateAssetBucketName as Qt, collectPublishedOutputNames as R, AssetError as Rn, AssetPublisher as Rt, isFinalSnapshotError as S, CFN_TEMPLATE_URL_LIMIT as Sn, s3BucketArn as St, makeCanonicalizePropertiesFn as T, uploadCfnTemplate as Tn, s3BucketRegionalDomainName as Tt, IAMRoleProvider as U, LocalInvokeBuildError as Un, loadPublishableAssetManifest as Ut, secretBearingStateKeyWarning as V, DependencyError as Vn, buildAssetRedirectMap as Vt, collectInlinePolicyNamesManagedBySiblings as W, LocalMigrateError as Wn, rewriteTemplateAssetReferences as Wt, slowCcOperationTimeoutMs as X, ProvisioningError as Xn, getBootstrapMarkerKey as Xt, CloudControlProvider as Y, PartialFailureError as Yn, ensureAssetStorage as Yt, disableInstanceApiTermination as Z, ResourceTimeoutError as Zn, parseBootstrapMarker as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, resolveStateBucketWithDefaultAndSource as _n, TEMPLATE_SOURCED_RULES as _t, DeploymentEventsStore as a, runDockerStreaming as an, normalizeAwsError as ar, resolveExplicitPhysicalId as at, ccRoutedFinalSnapshotError as b, warnDeprecatedNoPrefixCliFlag as bn, redactSecretsForState as bt, replayFailedOperations as c, Synthesizer as cn, isRetryableTransientError as cr, configBooleanRefusal as ct, updatePartialReason as d, getLegacyStateBucketName as dn, __exportAll as dr, replayWarn as dt, buildDenyExternalAccessPolicy as en, StackTerminationProtectionError as er, cfnRefValueFromPhysicalId as et, UNSPECIFIED_SKIP_REASON as f, resolveApp as fn, requireConfigArray as ft, computeImplicitDeleteEdges as g, resolveStateBucketWithDefault as gn, STATE_SOURCED_READBACK_RULES as gt, IMPLICIT_DELETE_DEPENDENCIES as h, resolveSkipPrefix as hn, STATE_SOURCED_CROSS_GENERATION_RULES as ht, DeploymentEventsReader as i, runDockerForeground as in, isCdkdError as ir, normalizeAwsTagsToCfn as it, bold as j, processStackMessages as jn, withRetry as jt, renderStatefulReason as k, derivePartitionAndUrlSuffix as kn, INTRINSIC_KEYS as kt, replayRollback as l, synthesisStatusMessage as ln, isThrottlingError as lr, configStringRefusal as lt, withResourceDeadline as m, resolveCaptureObservedState as mn, requireConfigString as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, formatDockerLoginError as nn, SynthesisError as nr, refStateLookupFromResource as nt, planFailedOps as o, AssetManifestLoader as on, withErrorHandling as or, assertRegionMatch as ot, deleteSkipReason as p, resolveAutoAssetStorage as pn, requireConfigObject as pt, findActionableSilentDrops as q, MissingCdkCliError as qn, AssetModeResolver as qt, DeployEngine as r, getDockerCmd as rn, formatError as rr, WAFv2WebACLProvider as rt, planRollback as s, getDockerImageBySourceHash as sn, isMarkedNonRetryable as sr, coerceCfnBoolean as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, buildDockerImage as tn, StateError as tr, getAccountInfo as tt, updatePartialMessage as u, getDefaultStateBucketName as un, markNonRetryable as ur, readConfigString as ut, PRE_DELETE_SNAPSHOT_TYPES as v, resolveUseCdkBootstrapAssets as vn, createSecretMasker as vt, unsupportedFinalSnapshotError as w, findLargeInlineResources as wn, s3BucketDualStackDomainName as wt, createPreDeleteFinalSnapshot as x, CFN_TEMPLATE_BODY_LIMIT as xn, scrubResourceRecord as xt, buildFinalSnapshotIdentifier as y, stateBucketExistenceConfirmed as yn, maskSecretsInText as yt, exportAliasCollisionScrubWarning as z, CdkdError as zn, stringifyValue as zt };
25668
+ //# sourceMappingURL=deploy-engine-BKSfYWps.js.map