@go-to-k/cdkd 0.284.32 → 0.284.34

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.
@@ -10416,6 +10416,136 @@ function isRecordedSecretExpression(expression) {
10416
10416
  function clearRecordedSecretExpressions() {
10417
10417
  recordedSecretExpressions$1.clear();
10418
10418
  }
10419
+ const crossStackAssociations = /* @__PURE__ */ new WeakMap();
10420
+ /** Poison for a key seen against two different (expression, plaintext) pairs. */
10421
+ const CONFLICTING_CROSS_STACK = Symbol("conflicting cross-stack association");
10422
+ /**
10423
+ * Separator for the composite keys {@link crossStackSourceKey} builds.
10424
+ *
10425
+ * A NUL rather than a printable character because no AWS export name, stack
10426
+ * name, output name, region or role ARN can contain one, so no two distinct
10427
+ * source leaves can spell a single key. A printable separator (`:` / `|`) does
10428
+ * occur inside a real export name — CDK's own convention is
10429
+ * `Stack:ExportName` — which would let one leaf's key be read as another's.
10430
+ */
10431
+ const CROSS_STACK_KEY_SEPARATOR = "\0";
10432
+ /** A non-empty literal string, or `undefined` for anything else. */
10433
+ function literalStringOrUndefined(value) {
10434
+ return typeof value === "string" && value !== "" ? value : void 0;
10435
+ }
10436
+ /**
10437
+ * The canonical key identifying a cross-stack source leaf, or `undefined` when
10438
+ * this leaf's identity is not LITERALLY COMPUTABLE from the source alone
10439
+ * (issue #2059).
10440
+ *
10441
+ * Both sides of {@link crossStackAssociations} call THIS function, which
10442
+ * is what makes the two keys byte-identical by construction: the resolver hands
10443
+ * it the raw intrinsic it is about to resolve, the redaction path hands it the
10444
+ * template source leaf at the position being persisted, and both are the same
10445
+ * template object. Deriving the writer's key from the resolver's RESOLVED
10446
+ * `exportName` / `stackName` instead would look equivalent and is not — the
10447
+ * persist path has only the source leaf, so the two spellings would have to be
10448
+ * proven equal at every slot rather than being the same string.
10449
+ *
10450
+ * REFUSAL IS THE POINT of the literal test. An export name that is itself an
10451
+ * `Fn::Sub` / `Fn::Join` / `Ref` resolves to something the persist path cannot
10452
+ * compute — it holds the unresolved template — so there is no honest key for it
10453
+ * and this returns `undefined`. The caller then falls back to today's behavior
10454
+ * (the skeleton pass, then the value scan) rather than guessing.
10455
+ *
10456
+ * The resolver's existing `origin` string is deliberately NOT reused: it is a
10457
+ * human-readable log label built from RESOLVED values and carrying prose
10458
+ * (`(producer X / Y)`), so it is neither derivable from the source leaf nor
10459
+ * stable.
10460
+ *
10461
+ * `Region` and `RoleArn` are OPTIONAL slots, and an ABSENT one keys as empty
10462
+ * while a PRESENT-but-non-literal one refuses. Absent has to be its own key
10463
+ * rather than being filled in with the resolver's own region: the persist path
10464
+ * cannot see that region, so a key built from it could not be recomputed.
10465
+ *
10466
+ * THE KEY IS THEREFORE NOT REGION-QUALIFIED, and an `Fn::ImportValue` key never
10467
+ * is at all — so it does NOT identify one producer on its own. Two stacks in
10468
+ * two regions carrying the identical leaf produce the identical key inside one
10469
+ * `cdkd deploy --all`, because `deploy.ts` builds a resolver per stack region.
10470
+ * An earlier revision of this note claimed the opposite ("one resolver region
10471
+ * answers them all"), and that false premise is exactly what let the store's
10472
+ * first shape certify one region's expression onto another region's resource.
10473
+ * What makes the key safe is not uniqueness but SCOPE:
10474
+ * {@link crossStackAssociations} is keyed by the resolution pass's own secrets
10475
+ * bag, so a key another pass recorded cannot be reached from here at all. Each
10476
+ * entry additionally carries the plaintext it resolved to, which is what
10477
+ * refuses a MISALIGNED entry inside one pass.
10478
+ *
10479
+ * A MULTI-KEY leaf (`{'Fn::ImportValue': 'X', Extra: 1}`) is the one exception
10480
+ * to "both sides compute the same string": the resolver reaches this function
10481
+ * having already selected the intrinsic, so it hands over a single-key object
10482
+ * and gets a key, while the redaction path sees the leaf as authored and
10483
+ * refuses on the `keys.length !== 1` test above. That asymmetry is FAIL-SAFE in
10484
+ * the only direction it can go — the writer records an association no reader
10485
+ * will ever look up — and such a leaf is not valid CloudFormation anyway.
10486
+ */
10487
+ function crossStackSourceKey(source) {
10488
+ const keys = Object.keys(source);
10489
+ if (keys.length !== 1) return void 0;
10490
+ const key = keys[0];
10491
+ if (key === "Fn::ImportValue") {
10492
+ const exportName = literalStringOrUndefined(source[key]);
10493
+ if (exportName === void 0) return void 0;
10494
+ return ["Fn::ImportValue", exportName].join(CROSS_STACK_KEY_SEPARATOR);
10495
+ }
10496
+ if (key === "Fn::GetStackOutput") {
10497
+ const args = source[key];
10498
+ if (!isPlainObject$2(args)) return void 0;
10499
+ const stackName = Object.hasOwn(args, "StackName") ? literalStringOrUndefined(args["StackName"]) : void 0;
10500
+ const outputName = Object.hasOwn(args, "OutputName") ? literalStringOrUndefined(args["OutputName"]) : void 0;
10501
+ if (stackName === void 0 || outputName === void 0) return void 0;
10502
+ const slots = [
10503
+ "Fn::GetStackOutput",
10504
+ stackName,
10505
+ outputName
10506
+ ];
10507
+ for (const optional of ["Region", "RoleArn"]) {
10508
+ const raw = Object.hasOwn(args, optional) ? args[optional] : void 0;
10509
+ if (raw === void 0 || raw === null) {
10510
+ slots.push("");
10511
+ continue;
10512
+ }
10513
+ const literal = literalStringOrUndefined(raw);
10514
+ if (literal === void 0) return void 0;
10515
+ slots.push(literal);
10516
+ }
10517
+ return slots.join(CROSS_STACK_KEY_SEPARATOR);
10518
+ }
10519
+ }
10520
+ /**
10521
+ * Remember, FOR THE PASS THAT OWNS `secrets`, that the cross-stack source leaf
10522
+ * keyed by `key` reads a producer value that IS the whole `{{resolve:...}}`
10523
+ * token `expression`, and that this pass saw it resolve to `plaintext`. Called
10524
+ * by the resolver, and only for a token it PROVED secret.
10525
+ *
10526
+ * `secrets` is the pass's own {@link RecordedSecretValues} bag, used as the
10527
+ * SCOPE KEY — the same object the redaction path will be handed. See
10528
+ * {@link crossStackAssociations} for why the scope, not the pairing, is what
10529
+ * makes this sound.
10530
+ */
10531
+ function recordCrossStackExpression(secrets, key, expression, plaintext) {
10532
+ if (!isSingleDynamicReferenceToken(expression)) return;
10533
+ let associations = crossStackAssociations.get(secrets);
10534
+ if (associations === void 0) {
10535
+ associations = /* @__PURE__ */ new Map();
10536
+ crossStackAssociations.set(secrets, associations);
10537
+ }
10538
+ const seen = associations.get(key);
10539
+ if (seen === void 0) {
10540
+ associations.set(key, {
10541
+ expression,
10542
+ plaintext
10543
+ });
10544
+ return;
10545
+ }
10546
+ if (typeof seen === "symbol") return;
10547
+ if (seen.expression !== expression || seen.plaintext !== plaintext) associations.set(key, CONFLICTING_CROSS_STACK);
10548
+ }
10419
10549
  /**
10420
10550
  * A resolved secret value shorter than this is NOT used as a redaction needle:
10421
10551
  * a 1-2 character plaintext (e.g. a secret whose JSON key holds `"0"`) would
@@ -10555,7 +10685,36 @@ function isDynamicReferenceString(value) {
10555
10685
  * leaks (both leaves are redacted, just onto one expression).
10556
10686
  */
10557
10687
  function isKnownSecretExpression(expression, secretExpressions) {
10558
- return expression.startsWith("{{resolve:secretsmanager:") || secretExpressions.has(expression) || isRecordedSecretExpression(expression);
10688
+ return isSecretExpressionByVerdictOrSpelling(expression) || secretExpressions.has(expression);
10689
+ }
10690
+ /**
10691
+ * The two arms of {@link isKnownSecretExpression} that need NO pass-local set:
10692
+ * `secretsmanager` by SPELLING, and anything this process PROVED secret.
10693
+ *
10694
+ * Split out so the resolver can ask the same question at the issue #2059
10695
+ * recording seam, where no `secretExpressions` set is in hand. It must not
10696
+ * acquire an argless default of its own — that is how a predicate silently
10697
+ * starts answering about a narrower population than its caller believes.
10698
+ *
10699
+ * The omitted arm costs the caller only REFUSALS. A cross-REGION `ssm`
10700
+ * `SecureString` is the one shape it can miss, because the producer-region
10701
+ * resolver is a GUEST and `pinSecretVerdict` deliberately writes nothing
10702
+ * process-wide from a guest (issue #1934's review) — so such a token is simply
10703
+ * not recorded at the seam, and its leaf falls back to the value scan.
10704
+ *
10705
+ * GUEST SUPPRESSION ALSO CUTS THE OTHER WAY, and saying only the above would be
10706
+ * one-sided. The same early return means a guest's DEFINITIVE PUBLIC verdict
10707
+ * never RETRACTS a memo either, so if the consumer's own resolver already
10708
+ * pinned that spelling as a `SecureString`, this answers `true` for a
10709
+ * producer-region parameter that is really a plain `String`. The outcome is
10710
+ * bounded to a spurious UPDATE (#1901's class) and can never be a plaintext:
10711
+ * the answer persisted is still an EXPRESSION, and the presence test beside
10712
+ * this one at the seam still requires the pass to have resolved it to a real
10713
+ * needle. Closing it means keying the verdict store by region, which is a
10714
+ * change to a store this function only reads.
10715
+ */
10716
+ function isSecretExpressionByVerdictOrSpelling(expression) {
10717
+ return expression.startsWith("{{resolve:secretsmanager:") || isRecordedSecretExpression(expression);
10559
10718
  }
10560
10719
  /**
10561
10720
  * The character class a `{{resolve:...}}` reference's INNER text is built from,
@@ -10828,6 +10987,121 @@ function intrinsicSkeletonPattern(source) {
10828
10987
  return body === void 0 ? void 0 : new RegExp(`^${body}$`);
10829
10988
  }
10830
10989
  }
10990
+ /** Poison for an expression this pass recorded against two different plaintexts. */
10991
+ const CONFLICTING_PLAINTEXT = Symbol("conflicting plaintext");
10992
+ /**
10993
+ * Walk a {@link RecordedSecretValues} the OTHER way: every expression the pass
10994
+ * recorded, against the plaintext it actually resolved to.
10995
+ *
10996
+ * This is condition 3's index, built ONCE per positioning call rather than
10997
+ * re-scanned per candidate. A collapsed LOSER is absent from it, which is the
10998
+ * case both callers exist to serve.
10999
+ *
11000
+ * It is NOT an inversion, because `secrets` need not be injective — one
11001
+ * expression CAN appear under two plaintexts. Taking the last such plaintext
11002
+ * would WEAKEN condition 3 (the scan it replaced refused when ANY entry
11003
+ * disagreed with `bag`), so a conflicting expression is poisoned to a sentinel
11004
+ * no bag can equal, which refuses it exactly as the scan did.
11005
+ *
11006
+ * The branch is HARD to reach from the resolver — one resolver's
11007
+ * `cachedDynamicReferences` yields one plaintext per expression, so a single
11008
+ * pass cannot produce two — but it is no longer unreachable from there since
11009
+ * that cache became per-resolver (issue #1933): two resolvers in two regions
11010
+ * legitimately resolve one expression to two different plaintexts, and a caller
11011
+ * merging their maps lands exactly here. It is reachable through this module's
11012
+ * API regardless, and it is FENCED, by the "recorded against MORE THAN ONE
11013
+ * plaintext" case. An earlier draft of this comment claimed the divergence was
11014
+ * unobservable, reasoning that `plaintextOf[E] === bag` implies
11015
+ * `secrets.get(bag) === E` so accepting and falling back agree. That misses the
11016
+ * case where a SECOND candidate also matches: accepting `E` then makes it two
11017
+ * matches, which condition 2 refuses, and the answers differ. Asserting
11018
+ * something cannot be fenced suppresses the attempt, so it needs the same
11019
+ * evidence a fence does.
11020
+ *
11021
+ * `has` is the whole test: `RecordedSecretValues` is keyed by plaintext, so
11022
+ * iterating it never yields one plaintext twice and a second sighting of an
11023
+ * expression is always a DIFFERENT plaintext.
11024
+ *
11025
+ * SHARED by {@link positionByIntrinsicSkeleton} and
11026
+ * {@link positionByCrossStackSource} (issue #2059) rather than copied into the
11027
+ * second: the poisoning rule is the subtle half of condition 3, and two copies
11028
+ * are two places for it to be relaxed independently.
11029
+ */
11030
+ function plaintextIndexOf(secrets) {
11031
+ const plaintextOf = /* @__PURE__ */ new Map();
11032
+ for (const [plaintext, expression] of secrets) plaintextOf.set(expression, plaintextOf.has(expression) ? CONFLICTING_PLAINTEXT : plaintext);
11033
+ return plaintextOf;
11034
+ }
11035
+ /**
11036
+ * Position a leaf whose SOURCE is a CROSS-STACK intrinsic object
11037
+ * (`Fn::ImportValue` / `Fn::GetStackOutput`), by looking its identity up in the
11038
+ * association the RESOLVER recorded while it read the producer (issue
11039
+ * [#2059](https://github.com/go-to-k/cdkd/issues/2059)).
11040
+ *
11041
+ * This is the residual {@link positionByIntrinsicSkeleton} leaves behind, and
11042
+ * it needs a different mechanism rather than one more skeleton arm.
11043
+ * {@link intrinsicSkeletonPattern} is a TEXT matcher over the source leaf's
11044
+ * literals, and these two intrinsics carry no text about their expression at
11045
+ * all: `Fn::ImportValue`'s only literal is the export NAME, and
11046
+ * `Fn::GetStackOutput`'s are `StackName` / `OutputName` / `Region`, none of
11047
+ * which bears any relation to the producer's `{{resolve:...}}` string. A
11048
+ * pure-wildcard skeleton is not a fallback either — {@link SKELETON_WILDCARD}
11049
+ * is `[^}]*`, which cannot cross a token's own `}}` — so it would match zero
11050
+ * candidates and always refuse, i.e. degrade to the collapse. The association
11051
+ * has to come from the one place that holds both halves at once, which is
11052
+ * {@link crossStackAssociations}.
11053
+ *
11054
+ * Three conditions, mirroring the ones next door, and each removing a different
11055
+ * way of being wrong:
11056
+ *
11057
+ * 1. The bag leaf's WHOLE value is a recorded secret plaintext — verbatim
11058
+ * condition 1 of {@link positionByIntrinsicSkeleton}. A leaf that merely
11059
+ * EMBEDS a secret is not this shape and must keep going to the value scan,
11060
+ * which rewrites just the substring. This is also what keeps a PUBLIC
11061
+ * reference out (issue #1901): the resolver records a plaintext only on a
11062
+ * proven-secret verdict, so a public parameter's value is not a key here.
11063
+ * 2. The association is ABOUT THIS BAG — the plaintext the WRITER recorded
11064
+ * beside the expression equals the bag leaf. Against another pass this is
11065
+ * belt-and-braces, since {@link crossStackAssociations} is scoped to the
11066
+ * pass and a foreign entry cannot be reached; within one pass it is the only
11067
+ * guard against a bag/source MISALIGNMENT.
11068
+ * 3. The match is not DEMONSTRABLY another value's expression — verbatim
11069
+ * condition 3 next door, over the same {@link plaintextIndexOf} index. It is
11070
+ * what fences a bag/source MISALIGNMENT: on a readback walk the bag leaf can
11071
+ * hold a different resource's secret while the source leaf still spells this
11072
+ * import, and an association recorded against a plaintext that is not this
11073
+ * bag is refused outright. The collapsed LOSER is absent from that index, so
11074
+ * it passes — which is the case this whole function exists to serve.
11075
+ *
11076
+ * There is deliberately NO "exactly one candidate" test (the neighbour's
11077
+ * condition 2): this is a LOOKUP rather than a search, so the ambiguity that
11078
+ * test exists to catch shows up here as a key recorded against two different
11079
+ * associations, which {@link recordCrossStackExpression} already poisons at
11080
+ * WRITE time.
11081
+ *
11082
+ * WHY THIS IS A POSITION CERTIFICATION AND NOT A WIDENING. The issue #1915
11083
+ * fences rejected an earlier attempt that took the SOURCE subtree whenever the
11084
+ * bag could not be vouched for, because it rewrote a `{Name: '', Value:
11085
+ * 'an-unrelated-literal'}` pair. Nothing here can do that: the answer is never
11086
+ * the source subtree, it is an expression a WRITER recorded against this exact
11087
+ * leaf identity; the arm fires for exactly two intrinsic spellings; and
11088
+ * condition 1 still demands that the bag leaf be a plaintext this pass
11089
+ * resolved. Every rejection degrades to {@link positionByIntrinsicSkeleton} and
11090
+ * then to the value scan, i.e. to today's behavior.
11091
+ */
11092
+ function positionByCrossStackSource(bag, source, secrets) {
11093
+ if (bag === "" || !secrets.has(bag)) return void 0;
11094
+ const key = crossStackSourceKey(source);
11095
+ if (key === void 0) return void 0;
11096
+ const associations = crossStackAssociations.get(secrets);
11097
+ if (associations === void 0) return void 0;
11098
+ const association = associations.get(key);
11099
+ if (association === void 0 || typeof association === "symbol") return void 0;
11100
+ if (association.plaintext !== bag) return void 0;
11101
+ const recordedPlaintext = plaintextIndexOf(secrets).get(association.expression);
11102
+ if (recordedPlaintext !== void 0 && recordedPlaintext !== bag) return void 0;
11103
+ return association.expression;
11104
+ }
10831
11105
  /**
10832
11106
  * Position a leaf whose SOURCE is an intrinsic OBJECT, by matching the shape of
10833
11107
  * that intrinsic against the expressions this process recorded as secrets
@@ -10894,9 +11168,7 @@ function positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions) {
10894
11168
  if (bag === "" || !secrets.has(bag)) return void 0;
10895
11169
  const pattern = intrinsicSkeletonPattern(source);
10896
11170
  if (!pattern) return void 0;
10897
- const CONFLICTING = Symbol("conflicting plaintext");
10898
- const plaintextOf = /* @__PURE__ */ new Map();
10899
- for (const [plaintext, expression] of secrets) plaintextOf.set(expression, plaintextOf.has(expression) ? CONFLICTING : plaintext);
11171
+ const plaintextOf = plaintextIndexOf(secrets);
10900
11172
  let matched;
10901
11173
  for (const candidate of /* @__PURE__ */ new Set([...secretExpressions, ...recordedSecretExpressions$1])) {
10902
11174
  if (candidate.length > MAX_SKELETON_CANDIDATE_LENGTH) return void 0;
@@ -11024,18 +11296,26 @@ function identityKeyFor(bag, source) {
11024
11296
  * record — which already holds the expressions — redacts it with no secret
11025
11297
  * fetch and no value matching.
11026
11298
  *
11027
- * A source leaf that is an intrinsic OBJECT (`Fn::Join` / `Fn::Sub`) has no
11028
- * string to copy, so it goes through {@link positionByIntrinsicSkeleton} first
11029
- * (issue #1916): when the intrinsic's literal parts describe exactly one of the
11030
- * recorded secret expressions, THAT is persisted. This is the dominant CDK
11031
- * shape an L2 secret token renders the ARN as a `Ref`, hence a join.
11032
- *
11033
- * The value scan is still applied wherever neither can answer: a leaf that
11034
- * merely EMBEDS a secret inside surrounding text, an intrinsic whose skeleton
11035
- * matches zero or several candidates, a diverged shape, a key the source lacks.
11036
- * So the passes are complementary rather than alternatives path where
11037
- * position is knowable, skeleton where the position is an intrinsic, value
11038
- * where neither is.
11299
+ * A source leaf that is an intrinsic OBJECT has no string to copy, so it goes
11300
+ * through two positioning passes before the value scan, in this order:
11301
+ *
11302
+ * - {@link positionByCrossStackSource} (issue #2059), for the two CROSS-STACK
11303
+ * spellings `Fn::ImportValue` / `Fn::GetStackOutput`. Those carry no text
11304
+ * about their expression at all, so the skeleton below structurally cannot
11305
+ * describe them; instead the RESOLVER recorded, while reading the producer,
11306
+ * which `{{resolve:...}}` token this exact leaf identity reads.
11307
+ * - {@link positionByIntrinsicSkeleton} (issue #1916), for `Fn::Join` /
11308
+ * `Fn::Sub`: when the intrinsic's literal parts describe exactly one of the
11309
+ * recorded secret expressions, THAT is persisted. This is the dominant CDK
11310
+ * shape an L2 secret token renders the ARN as a `Ref`, hence a join.
11311
+ *
11312
+ * The value scan is still applied wherever none can answer: a leaf that merely
11313
+ * EMBEDS a secret inside surrounding text, an intrinsic whose skeleton matches
11314
+ * zero or several candidates, a cross-stack leaf whose identity is not
11315
+ * literally computable, a diverged shape, a key the source lacks. So the passes
11316
+ * are complementary rather than alternatives — path where position is knowable,
11317
+ * association where the position is a cross-stack read, skeleton where it is a
11318
+ * describable intrinsic, value where none is.
11039
11319
  */
11040
11320
  function redactByPath(bag, source, secrets, rules, secretExpressions) {
11041
11321
  if (isDynamicReferenceString(source) && typeof bag === "string") {
@@ -11046,6 +11326,8 @@ function redactByPath(bag, source, secrets, rules, secretExpressions) {
11046
11326
  return redactSecretsForState(bag, secrets);
11047
11327
  }
11048
11328
  if (typeof bag === "string" && isPlainObject$2(source)) {
11329
+ const certified = positionByCrossStackSource(bag, source, secrets);
11330
+ if (certified !== void 0) return certified;
11049
11331
  const positioned = positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions);
11050
11332
  if (positioned !== void 0) return positioned;
11051
11333
  }
@@ -15182,7 +15464,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
15182
15464
  * the producer's rather than the caller's — which is exactly what issue #2057
15183
15465
  * says the other two copies get wrong.
15184
15466
  */
15185
- async reresolveCrossStackValue(value, producerRegion, context, origin) {
15467
+ async reresolveCrossStackValue(value, producerRegion, context, origin, sourceKey) {
15186
15468
  if (!carriesDynamicReference(value)) return value;
15187
15469
  const resolver = this.resolverForProducerRegion(producerRegion);
15188
15470
  const walk = async (v) => {
@@ -15200,7 +15482,9 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
15200
15482
  return v;
15201
15483
  };
15202
15484
  this.logger.debug(`Re-resolving dynamic reference(s) in ${this.maskSecretsForLog(origin, context)}`);
15203
- return await walk(value);
15485
+ const reresolved = await walk(value);
15486
+ if (sourceKey !== void 0 && typeof value === "string" && isSingleDynamicReferenceToken(value) && typeof reresolved === "string" && context.recordedSecretValues?.has(reresolved) === true && isSecretExpressionByVerdictOrSpelling(value)) recordCrossStackExpression(context.recordedSecretValues, sourceKey, value, reresolved);
15487
+ return reresolved;
15204
15488
  }
15205
15489
  /**
15206
15490
  * The resolver that must answer for a PRODUCER region — `this` when the
@@ -15283,6 +15567,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
15283
15567
  * Searches all other stacks for an exported output with the given name.
15284
15568
  */
15285
15569
  async resolveImportValue(importValueArg, context) {
15570
+ const sourceKey = crossStackSourceKey({ "Fn::ImportValue": importValueArg });
15286
15571
  const exportName = await this.resolveValue(importValueArg, context);
15287
15572
  if (typeof exportName !== "string") throw new Error(`Fn::ImportValue: export name must resolve to a string, got ${typeof exportName}`);
15288
15573
  if (!context.stateBackend) throw new Error("Fn::ImportValue: state backend is required for cross-stack references");
@@ -15299,7 +15584,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
15299
15584
  if (entry && (!context.stackName || entry.producerStack !== context.stackName)) {
15300
15585
  this.recordImport(context, exportName, entry.producerStack, entry.producerRegion);
15301
15586
  this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from index: ${entry.producerStack} / ${entry.producerRegion}; ${carriesDynamicReference(entry.value) ? "redacted dynamic reference" : "literal value"})`);
15302
- return await this.reresolveCrossStackValue(entry.value, entry.producerRegion, context, `Fn::ImportValue '${exportName}' (producer ${entry.producerStack} / ${entry.producerRegion})`);
15587
+ return await this.reresolveCrossStackValue(entry.value, entry.producerRegion, context, `Fn::ImportValue '${exportName}' (producer ${entry.producerStack} / ${entry.producerRegion})`, sourceKey);
15303
15588
  }
15304
15589
  }
15305
15590
  const allStacks = await context.stateBackend.listStacks();
@@ -15346,7 +15631,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
15346
15631
  continue;
15347
15632
  }
15348
15633
  }
15349
- if (found) return await this.reresolveCrossStackValue(found.value, found.lookupRegion, context, `Fn::ImportValue '${exportName}' (producer ${found.refStack} / ${found.lookupRegion})`);
15634
+ if (found) return await this.reresolveCrossStackValue(found.value, found.lookupRegion, context, `Fn::ImportValue '${exportName}' (producer ${found.refStack} / ${found.lookupRegion})`, sourceKey);
15350
15635
  if (this.cfnFallback) {
15351
15636
  const cfnExport = await this.lookupCfnExport(exportName, context);
15352
15637
  if (cfnExport) {
@@ -15551,6 +15836,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
15551
15836
  const args = arg;
15552
15837
  if (!("StackName" in args)) throw new Error("Fn::GetStackOutput: StackName is required");
15553
15838
  if (!("OutputName" in args)) throw new Error("Fn::GetStackOutput: OutputName is required");
15839
+ const sourceKey = crossStackSourceKey({ "Fn::GetStackOutput": args });
15554
15840
  const stackName = await this.resolveValue(args["StackName"], context);
15555
15841
  if (typeof stackName !== "string" || stackName === "") throw new Error(`Fn::GetStackOutput: StackName must resolve to a non-empty string, got ${typeof stackName}`);
15556
15842
  const outputName = await this.resolveValue(args["OutputName"], context);
@@ -15598,7 +15884,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
15598
15884
  this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${region}, OutputName=${loggedOutputName}${roleArn ? `, RoleArn=${roleArn}` : ""} (${carriesDynamicReference(value) ? "redacted dynamic reference" : "literal value"})`);
15599
15885
  if (!roleArn) this.recordOutputRead(context, stackName, region, outputName);
15600
15886
  if (roleArn && !context.skipDynamicReferences && carriesDynamicReference(value)) throw markNonRetryable(new CrossAccountSecretRefusalError(`Fn::GetStackOutput: output '${outputName}' of stack '${stackName}' (${region}) is a redacted dynamic reference, and this is a CROSS-ACCOUNT reference (RoleArn ${roleArn}). cdkd will not resolve a producer account's secret with the consumer's credentials — a same-named secret in the consumer account would answer instead. Export a non-secret value (e.g. the secret's ARN) and resolve it in the consumer stack, or reference the producer stack from within its own account.`));
15601
- return await this.reresolveCrossStackValue(value, region, context, `Fn::GetStackOutput '${outputName}' (producer ${stackName} / ${region})`);
15887
+ return await this.reresolveCrossStackValue(value, region, context, `Fn::GetStackOutput '${outputName}' (producer ${stackName} / ${region})`, sourceKey);
15602
15888
  }
15603
15889
  /**
15604
15890
  * Push a resolved `Fn::GetStackOutput` into the consumer's
@@ -16987,7 +17273,7 @@ var CloudControlProvider = class {
16987
17273
  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);
16988
17274
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
16989
17275
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
16990
- const { ASGProvider } = await import("./asg-provider-BVzQ7mQX.js").then((n) => n.n);
17276
+ const { ASGProvider } = await import("./asg-provider-DZ_xdKkZ.js").then((n) => n.n);
16991
17277
  return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
16992
17278
  }
16993
17279
  const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
@@ -25900,7 +26186,7 @@ const FLUSH_INTERVAL_MS = 2e3;
25900
26186
  const FLUSH_EVENT_THRESHOLD = 50;
25901
26187
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
25902
26188
  function getCdkdVersion() {
25903
- return "0.284.32";
26189
+ return "0.284.34";
25904
26190
  }
25905
26191
  /**
25906
26192
  * Generate a time-sortable unique run id, e.g.
@@ -28521,4 +28807,4 @@ var DeployEngine = class {
28521
28807
 
28522
28808
  //#endregion
28523
28809
  export { endCommandInterruptScope as $, ConfigError as $n, buildAssetRedirectMap as $t, MULTI_REGION_RECREATE_BLOCKED_TYPES as A, resolveStateBucketWithDefaultAndSource as An, maskSecretsInError as At, collectDeclaredOutputNames as B, PARTITION_TABLE as Bn, DiffCalculator as Bt, ccRoutedFinalSnapshotError as C, getDefaultStateBucketName as Cn, isThrottlingError as Cr, STATE_SOURCED_CROSS_GENERATION_RULES as Ct, unsupportedFinalSnapshotError as D, resolveCaptureObservedState as Dn, dynamicReferenceTokens as Dt, refusesFinalSnapshot as E, resolveAutoAssetStorage as En, createSecretMasker as Et, cyan as F, CFN_TEMPLATE_URL_LIMIT as Fn, s3BucketDomainName as Ft, stateKeySecretExposure as G, clearBucketRegionCache as Gn, TemplateParser as Gt, exportAliasCollisionScrubWarning as H, derivePartitionAndUrlSuffix as Hn, describeTypeWithThrottleRetry as Ht, gray as I, MIGRATE_TMP_PREFIX as In, s3BucketDualStackDomainName as It, clearOnUpdateRemoval as J, getAwsClients as Jn, rebuildClientForBucketRegion as Jt, IAMRoleProvider as K, resolveBucketRegion as Kn, LockManager as Kt, green as L, findLargeInlineResources as Ln, s3BucketRegionalDomainName as Lt, renderStatefulReason as M, stateBucketExistenceConfirmed as Mn, redactSecretsForState as Mt, formatResourceLine as N, warnDeprecatedNoPrefixCliFlag as Nn, scrubResourceRecord as Nt, makeCanonicalizePropertiesFn as O, resolveSkipPrefix as On, errorCauseChain as Ot, bold as P, CFN_TEMPLATE_BODY_LIMIT as Pn, s3BucketArn as Pt, beginCommandInterruptScope as Q, CdkdError as Qn, WorkGraph as Qt, red as R, uploadCfnTemplate as Rn, s3BucketWebsiteUrl as Rt, buildFinalSnapshotIdentifier as S, synthesisStatusMessage as Sn, isRetryableTransientError as Sr, requireConfigString as St, isFinalSnapshotError as T, resolveApp as Tn, __exportAll as Tr, TEMPLATE_SOURCED_RULES as Tt, isExportAliasCollision as U, AssemblyReader as Un, withRetry as Ut, collectPublishedOutputNames as V, canonicalizeRegion as Vn, INTRINSIC_KEYS as Vt, secretBearingStateKeyWarning as W, processStackMessages as Wn, DagBuilder as Wt, findActionableSilentDrops as X, setAwsClients as Xn, AssetPublisher as Xt, ProviderRegistry as Y, resetAwsClients as Yn, shouldRetainResource as Yt, findSilentDropProperties as Z, AssetError as Zn, stringifyValue as Zt, maskingRetryLogger as _, runDockerForeground as _n, formatError as _r, configStringRefusal as _t, DeploymentEventsStore as a, AssetModeResolver as an, LocalStartServiceError as ar, isTerminationProtectionPropagationError as at, ATOMIC_FINAL_SNAPSHOT_TYPES as b, getDockerImageBySourceHash as bn, withErrorHandling as br, requireConfigArray as bt, planRollback as c, getBootstrapMarkerKey as cn, NestedStackChildDirectDestroyError as cr, cfnRefValueFromPhysicalId as ct, replayRollback as d, validateAssetBucketName as dn, ResourceTimeoutError as dr, WAFv2WebACLProvider as dt, createAssetRedirectResolver as en, CrossAccountSecretRefusalError as er, isInterruptedWaitError as et, updatePartialMessage as f, validateContainerRepoName as fn, ResourceUpdateNotSupportedError as fr, normalizeAwsTagsToCfn as ft, withResourceDeadline as g, getDockerCmd as gn, SynthesisError as gr, configBooleanRefusal as gt, deleteSkipReason as h, formatDockerLoginError as hn, StateError as hr, coerceCfnBoolean as ht, DeploymentEventsReader as i, stripControlChars as in, LocalMigrateError as ir, disableInstanceApiTermination as it, isStatefulRecreateTargetSync as j, resolveUseCdkBootstrapAssets as jn, maskSecretsInText as jt, extractDeploymentEventError as k, resolveStateBucketWithDefault as kn, isSingleDynamicReferenceToken as kt, producerRegionsFromState as l, parseBootstrapMarker as ln, PartialFailureError as lr, getAccountInfo as lt, UNSPECIFIED_SKIP_REASON as m, buildDockerImage as mn, StackTerminationProtectionError as mr, assertRegionMatch as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, rewriteTemplateAssetReferences as nn, DeployCancelledError as nr, CloudControlProvider as nt, classifyReplaySecretRegion as o, BOOTSTRAP_MARKER_PREFIX as on, LockError as or, IntrinsicFunctionResolver as ot, updatePartialReason as p, buildDenyExternalAccessPolicy as pn, StackHasActiveImportsError as pr, resolveExplicitPhysicalId as pt, collectInlinePolicyNamesManagedBySiblings as q, AwsClients as qn, S3StateBackend as qt, DeployEngine as r, escapeRegExp$1 as rn, LocalInvokeBuildError as rr, slowCcOperationTimeoutMs as rt, planFailedOps as s, ensureAssetStorage as sn, MissingCdkCliError as sr, carriesDynamicReference as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, loadPublishableAssetManifest as tn, DependencyError as tr, startInterruptWatch as tt, replayFailedOperations as u, readBootstrapMarkerBody as un, ProvisioningError as ur, refStateLookupFromResource as ut, IMPLICIT_DELETE_DEPENDENCIES as v, runDockerStreaming as vn, isCdkdError as vr, readConfigString as vt, createPreDeleteFinalSnapshot as w, getLegacyStateBucketName as wn, markNonRetryable as wr, STATE_SOURCED_READBACK_RULES as wt, PRE_DELETE_SNAPSHOT_TYPES as x, Synthesizer as xn, isMarkedNonRetryable as xr, requireConfigObject as xt, computeImplicitDeleteEdges as y, AssetManifestLoader as yn, normalizeAwsError as yr, replayWarn as yt, yellow as z, expectedOwnerParam as zn, applyRoleArnIfSet as zt };
28524
- //# sourceMappingURL=deploy-engine-DsUQ4GxO.js.map
28810
+ //# sourceMappingURL=deploy-engine-BxP9k1OS.js.map