@go-to-k/cdkd 0.284.31 → 0.284.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{asg-provider-DJvcY8lj.js → asg-provider-CZc9Mtx2.js} +2 -2
- package/dist/{asg-provider-DJvcY8lj.js.map → asg-provider-CZc9Mtx2.js.map} +1 -1
- package/dist/cli.js +620 -48
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-DYdEWX-s.js → deploy-engine-mhGFd83I.js} +439 -54
- package/dist/deploy-engine-mhGFd83I.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-DYdEWX-s.js.map +0 -1
|
@@ -771,7 +771,7 @@ function formatDuration(ms) {
|
|
|
771
771
|
* "the referenced thing does not exist" miss (issue
|
|
772
772
|
* [#1740](https://github.com/go-to-k/cdkd/issues/1740)).
|
|
773
773
|
*
|
|
774
|
-
* The distinction exists for
|
|
774
|
+
* The distinction exists for one consumer of THIS class: `Fn::Sub`'s variable
|
|
775
775
|
* resolution, which speculatively tries `Ref` and then `Fn::GetAtt` and keeps
|
|
776
776
|
* the raw `${...}` placeholder when neither resolves. That warn-and-keep is the
|
|
777
777
|
* long-standing, deliberate behavior for a genuinely unknown variable — but a
|
|
@@ -782,11 +782,15 @@ function formatDuration(ms) {
|
|
|
782
782
|
*
|
|
783
783
|
* Throwing this class rather than a bare `Error` is what lets that catch
|
|
784
784
|
* re-raise a refusal (carrying its own message and remedy) while leaving the
|
|
785
|
-
* not-found path on warn-and-keep. Nothing else branches on
|
|
785
|
+
* not-found path on warn-and-keep. Nothing else branches on THIS class;
|
|
786
|
+
* `cdkd scrub` branches on the {@link CrossAccountSecretRefusalError} SUBCLASS,
|
|
787
|
+
* for the reason group 3 below gives.
|
|
786
788
|
*
|
|
787
|
-
* **Throw sites split into
|
|
789
|
+
* **Throw sites split into three groups, and the enumeration is worth keeping
|
|
788
790
|
* accurate** — an out-of-date one reads as "these are all of them", which is
|
|
789
|
-
* how the #1730 site below went unlisted for three releases
|
|
791
|
+
* how the #1730 site below went unlisted for three releases, and how the
|
|
792
|
+
* cross-account `Fn::GetStackOutput` site in group 3 went unlisted from the
|
|
793
|
+
* day it shipped. All live in
|
|
790
794
|
* `src/deployment/intrinsic-function-resolver.ts`:
|
|
791
795
|
*
|
|
792
796
|
* 1. Reachable from `Fn::Sub`'s `${LogicalId.Attribute}` form, i.e. the ones
|
|
@@ -800,29 +804,77 @@ function formatDuration(ms) {
|
|
|
800
804
|
* inspects it: `resolveSplit`'s two refusals of a non-string value (#1874).
|
|
801
805
|
* `Fn::Sub` cannot syntactically contain an `Fn::Split`, so those change no
|
|
802
806
|
* behavior by being this class.
|
|
807
|
+
* 3. PERMANENT — the one site where no user action and no re-run can make the
|
|
808
|
+
* read succeed: `resolveGetStackOutput`'s cross-account refusal to resolve a
|
|
809
|
+
* producer account's redacted dynamic reference with the consumer's
|
|
810
|
+
* credentials. It throws the SUBCLASS
|
|
811
|
+
* {@link CrossAccountSecretRefusalError} rather than this class, because
|
|
812
|
+
* every site in groups 1 and 2 is USER-FIXABLE (correct the stale
|
|
813
|
+
* placeholder ARN, deploy the producer so STS resolves, enrich the
|
|
814
|
+
* `Fn::GetAtt`, drop `--strict-getatt`, fix the malformed `Fn::Split`) and a
|
|
815
|
+
* consumer that treats "permanent" as a property of the CLASS silently
|
|
816
|
+
* downgrades all five. `cdkd scrub`'s cross-stack pre-pass is that consumer:
|
|
817
|
+
* it records a permanent refusal as a FINDING and scrubs the rest of the
|
|
818
|
+
* stack, but must REFUSE the stack for a fixable one, since a re-run after
|
|
819
|
+
* the fix would scrub it (issue
|
|
820
|
+
* [#2133](https://github.com/go-to-k/cdkd/issues/2133) review). Match on the
|
|
821
|
+
* subclass, never on this class.
|
|
803
822
|
*
|
|
804
823
|
* The class is deliberately NOT `markNonRetryable` at construction, unlike
|
|
805
824
|
* {@link ResourceUpdateNotSupportedError}: EXACTLY ONE of its throw sites is
|
|
806
825
|
* genuinely time-dependent — the fabricated-account guard, where
|
|
807
826
|
* `getAccountInfo` caches a fabricated answer for only 10s precisely so a
|
|
808
827
|
* later attempt can heal — so a constructor-level marker would wrongly make
|
|
809
|
-
* that one terminal. Every OTHER site marks at its own `throw`: all
|
|
828
|
+
* that one terminal. Every OTHER site marks at its own `throw`: all six
|
|
810
829
|
* decide from inputs a retry cannot change (a persisted state record, an
|
|
811
|
-
* attribute-name suffix, a CLI flag, an already-resolved value's type
|
|
812
|
-
*
|
|
830
|
+
* attribute-name suffix, a CLI flag, an already-resolved value's type, a
|
|
831
|
+
* template's literal `RoleArn`), and all six interpolate
|
|
832
|
+
* template-controlled text into their message, which the
|
|
813
833
|
* SUBSTRING-matching retry classifiers can read as transient (issue #1838 —
|
|
814
834
|
* a logical id like `MyDependencyViolationHandler` is enough). So the split is
|
|
815
835
|
* "which SITE can heal", not "which class"; do not read the unmarked class as
|
|
816
836
|
* a statement that these refusals are retryable.
|
|
817
837
|
*/
|
|
818
838
|
var IntrinsicResolutionRefusalError = class IntrinsicResolutionRefusalError extends CdkdError {
|
|
819
|
-
constructor(message, cause) {
|
|
820
|
-
super(message,
|
|
839
|
+
constructor(message, cause, code = "INTRINSIC_RESOLUTION_REFUSAL") {
|
|
840
|
+
super(message, code, cause);
|
|
821
841
|
this.name = "IntrinsicResolutionRefusalError";
|
|
822
842
|
Object.setPrototypeOf(this, IntrinsicResolutionRefusalError.prototype);
|
|
823
843
|
}
|
|
824
844
|
};
|
|
825
845
|
/**
|
|
846
|
+
* The ONE refusal in group 3 of {@link IntrinsicResolutionRefusalError}: a
|
|
847
|
+
* cross-account `Fn::GetStackOutput` whose stored value is a redacted dynamic
|
|
848
|
+
* reference (issue [#2133](https://github.com/go-to-k/cdkd/issues/2133)
|
|
849
|
+
* review).
|
|
850
|
+
*
|
|
851
|
+
* A SUBCLASS rather than a flag, so the two properties stay independent:
|
|
852
|
+
* `instanceof IntrinsicResolutionRefusalError` is still true, which is what
|
|
853
|
+
* `resolveSub`'s catch re-raises on (making this refusal propagate out of an
|
|
854
|
+
* `Fn::Sub` instead of being laundered into a literal `${...}`), while
|
|
855
|
+
* consumers that need "no re-run can change this" match on THIS class and
|
|
856
|
+
* therefore cannot capture the five user-fixable siblings.
|
|
857
|
+
*
|
|
858
|
+
* `code` is distinct for the same reason a message is not: a consumer keying
|
|
859
|
+
* on `INTRINSIC_RESOLUTION_REFUSAL` would capture every sibling, and one
|
|
860
|
+
* keying on message text breaks the moment the wording improves.
|
|
861
|
+
*
|
|
862
|
+
* `cdkd scrub` is the consumer today. Its cross-stack pre-pass records a
|
|
863
|
+
* refusal of THIS class as an unverifiable FINDING — the rest of the stack is
|
|
864
|
+
* still scrubbed, and the run exits non-zero — because refusing the whole
|
|
865
|
+
* stack would strand every other secret in it forever. A sibling refusal
|
|
866
|
+
* (a stale placeholder ARN, an unenriched `Fn::GetAtt`, a malformed
|
|
867
|
+
* `Fn::Split`, all reachable through an `Fn::Sub`-built export name) must
|
|
868
|
+
* REFUSE instead, since the user can fix the cause and re-run.
|
|
869
|
+
*/
|
|
870
|
+
var CrossAccountSecretRefusalError = class CrossAccountSecretRefusalError extends IntrinsicResolutionRefusalError {
|
|
871
|
+
constructor(message, cause) {
|
|
872
|
+
super(message, cause, "INTRINSIC_RESOLUTION_REFUSAL_CROSS_ACCOUNT_SECRET");
|
|
873
|
+
this.name = "CrossAccountSecretRefusalError";
|
|
874
|
+
Object.setPrototypeOf(this, CrossAccountSecretRefusalError.prototype);
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
/**
|
|
826
878
|
* Dependency resolution errors
|
|
827
879
|
*/
|
|
828
880
|
var DependencyError = class DependencyError extends CdkdError {
|
|
@@ -10364,6 +10416,136 @@ function isRecordedSecretExpression(expression) {
|
|
|
10364
10416
|
function clearRecordedSecretExpressions() {
|
|
10365
10417
|
recordedSecretExpressions$1.clear();
|
|
10366
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
|
+
}
|
|
10367
10549
|
/**
|
|
10368
10550
|
* A resolved secret value shorter than this is NOT used as a redaction needle:
|
|
10369
10551
|
* a 1-2 character plaintext (e.g. a secret whose JSON key holds `"0"`) would
|
|
@@ -10503,7 +10685,36 @@ function isDynamicReferenceString(value) {
|
|
|
10503
10685
|
* leaks (both leaves are redacted, just onto one expression).
|
|
10504
10686
|
*/
|
|
10505
10687
|
function isKnownSecretExpression(expression, secretExpressions) {
|
|
10506
|
-
return 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);
|
|
10507
10718
|
}
|
|
10508
10719
|
/**
|
|
10509
10720
|
* The character class a `{{resolve:...}}` reference's INNER text is built from,
|
|
@@ -10776,6 +10987,121 @@ function intrinsicSkeletonPattern(source) {
|
|
|
10776
10987
|
return body === void 0 ? void 0 : new RegExp(`^${body}$`);
|
|
10777
10988
|
}
|
|
10778
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
|
+
}
|
|
10779
11105
|
/**
|
|
10780
11106
|
* Position a leaf whose SOURCE is an intrinsic OBJECT, by matching the shape of
|
|
10781
11107
|
* that intrinsic against the expressions this process recorded as secrets
|
|
@@ -10842,9 +11168,7 @@ function positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions) {
|
|
|
10842
11168
|
if (bag === "" || !secrets.has(bag)) return void 0;
|
|
10843
11169
|
const pattern = intrinsicSkeletonPattern(source);
|
|
10844
11170
|
if (!pattern) return void 0;
|
|
10845
|
-
const
|
|
10846
|
-
const plaintextOf = /* @__PURE__ */ new Map();
|
|
10847
|
-
for (const [plaintext, expression] of secrets) plaintextOf.set(expression, plaintextOf.has(expression) ? CONFLICTING : plaintext);
|
|
11171
|
+
const plaintextOf = plaintextIndexOf(secrets);
|
|
10848
11172
|
let matched;
|
|
10849
11173
|
for (const candidate of /* @__PURE__ */ new Set([...secretExpressions, ...recordedSecretExpressions$1])) {
|
|
10850
11174
|
if (candidate.length > MAX_SKELETON_CANDIDATE_LENGTH) return void 0;
|
|
@@ -10972,18 +11296,26 @@ function identityKeyFor(bag, source) {
|
|
|
10972
11296
|
* record — which already holds the expressions — redacts it with no secret
|
|
10973
11297
|
* fetch and no value matching.
|
|
10974
11298
|
*
|
|
10975
|
-
* A source leaf that is an intrinsic OBJECT
|
|
10976
|
-
*
|
|
10977
|
-
*
|
|
10978
|
-
*
|
|
10979
|
-
*
|
|
10980
|
-
*
|
|
10981
|
-
*
|
|
10982
|
-
*
|
|
10983
|
-
*
|
|
10984
|
-
*
|
|
10985
|
-
*
|
|
10986
|
-
*
|
|
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.
|
|
10987
11319
|
*/
|
|
10988
11320
|
function redactByPath(bag, source, secrets, rules, secretExpressions) {
|
|
10989
11321
|
if (isDynamicReferenceString(source) && typeof bag === "string") {
|
|
@@ -10994,6 +11326,8 @@ function redactByPath(bag, source, secrets, rules, secretExpressions) {
|
|
|
10994
11326
|
return redactSecretsForState(bag, secrets);
|
|
10995
11327
|
}
|
|
10996
11328
|
if (typeof bag === "string" && isPlainObject$2(source)) {
|
|
11329
|
+
const certified = positionByCrossStackSource(bag, source, secrets);
|
|
11330
|
+
if (certified !== void 0) return certified;
|
|
10997
11331
|
const positioned = positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions);
|
|
10998
11332
|
if (positioned !== void 0) return positioned;
|
|
10999
11333
|
}
|
|
@@ -13243,6 +13577,12 @@ function buildUnknownIntrinsicError(key) {
|
|
|
13243
13577
|
* `Record<string, unknown>` and deliberately NOT coerced to string — a
|
|
13244
13578
|
* list-valued `Fn::GetAtt` persists a JSON array — so a secret-bearing output
|
|
13245
13579
|
* is not always a bare string.
|
|
13580
|
+
*
|
|
13581
|
+
* EXPORTED for `cdkd scrub` (issue
|
|
13582
|
+
* [#2133](https://github.com/go-to-k/cdkd/issues/2133)), which asks the inverse
|
|
13583
|
+
* question of the same value: a cross-stack read that comes back carrying NO
|
|
13584
|
+
* dynamic reference is one scrub could not turn into a needle, because a needle
|
|
13585
|
+
* is only ever recorded by resolving a `{{resolve:...}}` expression.
|
|
13246
13586
|
*/
|
|
13247
13587
|
function carriesDynamicReference(value) {
|
|
13248
13588
|
if (typeof value === "string") return value.includes("{{resolve:");
|
|
@@ -13371,6 +13711,14 @@ const FABRICATED_ACCOUNT_INFO_TTL_MS = 1e4;
|
|
|
13371
13711
|
*/
|
|
13372
13712
|
const MAX_DYNAMIC_REFERENCE_THROTTLE_RETRIES = 4;
|
|
13373
13713
|
/**
|
|
13714
|
+
* How many producer output KEYS an `Fn::GetStackOutput` not-found error may
|
|
13715
|
+
* enumerate (issue #2133 review). See {@link
|
|
13716
|
+
* IntrinsicFunctionResolver.describeAvailableOutputs} for why the list is
|
|
13717
|
+
* bounded at all; the value is "enough to fix a typo, few enough that one error
|
|
13718
|
+
* cannot dump a producer's whole key space".
|
|
13719
|
+
*/
|
|
13720
|
+
const MAX_LISTED_AVAILABLE_OUTPUTS = 10;
|
|
13721
|
+
/**
|
|
13374
13722
|
* Test seam: overriding `sleep` lets unit tests drive the backoff schedule
|
|
13375
13723
|
* without real waits (mirrors `describeTypeRetryDelays`).
|
|
13376
13724
|
*/
|
|
@@ -14975,7 +15323,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
14975
15323
|
const resolved1 = await this.resolveValue(value1, context);
|
|
14976
15324
|
const resolved2 = await this.resolveValue(value2, context);
|
|
14977
15325
|
const result = JSON.stringify(resolved1) === JSON.stringify(resolved2);
|
|
14978
|
-
this.logger.debug(`Resolved Fn::Equals: ${JSON.stringify(resolved1)} === ${JSON.stringify(resolved2)} -> ${result}`);
|
|
15326
|
+
this.logger.debug(`Resolved Fn::Equals: ${this.maskSecretsForLog(JSON.stringify(resolved1), context)} === ${this.maskSecretsForLog(JSON.stringify(resolved2), context)} -> ${result}`);
|
|
14979
15327
|
return result;
|
|
14980
15328
|
}
|
|
14981
15329
|
/**
|
|
@@ -15116,7 +15464,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
15116
15464
|
* the producer's rather than the caller's — which is exactly what issue #2057
|
|
15117
15465
|
* says the other two copies get wrong.
|
|
15118
15466
|
*/
|
|
15119
|
-
async reresolveCrossStackValue(value, producerRegion, context, origin) {
|
|
15467
|
+
async reresolveCrossStackValue(value, producerRegion, context, origin, sourceKey) {
|
|
15120
15468
|
if (!carriesDynamicReference(value)) return value;
|
|
15121
15469
|
const resolver = this.resolverForProducerRegion(producerRegion);
|
|
15122
15470
|
const walk = async (v) => {
|
|
@@ -15133,8 +15481,10 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
15133
15481
|
}
|
|
15134
15482
|
return v;
|
|
15135
15483
|
};
|
|
15136
|
-
this.logger.debug(`Re-resolving dynamic reference(s) in ${origin}`);
|
|
15137
|
-
|
|
15484
|
+
this.logger.debug(`Re-resolving dynamic reference(s) in ${this.maskSecretsForLog(origin, context)}`);
|
|
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;
|
|
15138
15488
|
}
|
|
15139
15489
|
/**
|
|
15140
15490
|
* The resolver that must answer for a PRODUCER region — `this` when the
|
|
@@ -15217,26 +15567,28 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
15217
15567
|
* Searches all other stacks for an exported output with the given name.
|
|
15218
15568
|
*/
|
|
15219
15569
|
async resolveImportValue(importValueArg, context) {
|
|
15570
|
+
const sourceKey = crossStackSourceKey({ "Fn::ImportValue": importValueArg });
|
|
15220
15571
|
const exportName = await this.resolveValue(importValueArg, context);
|
|
15221
15572
|
if (typeof exportName !== "string") throw new Error(`Fn::ImportValue: export name must resolve to a string, got ${typeof exportName}`);
|
|
15222
15573
|
if (!context.stateBackend) throw new Error("Fn::ImportValue: state backend is required for cross-stack references");
|
|
15223
|
-
this.
|
|
15574
|
+
const loggedExportName = this.maskSecretsForLog(exportName, context);
|
|
15575
|
+
this.logger.debug(`Resolving Fn::ImportValue: ${loggedExportName}`);
|
|
15224
15576
|
if (context.exportIndex) {
|
|
15225
15577
|
let entry;
|
|
15226
15578
|
try {
|
|
15227
15579
|
entry = await context.exportIndex.lookup(exportName);
|
|
15228
15580
|
} catch (err) {
|
|
15229
|
-
this.logger.warn(`Exports index lookup failed for '${
|
|
15581
|
+
this.logger.warn(`Exports index lookup failed for '${loggedExportName}': ${err instanceof Error ? err.message : String(err)}; falling back to state.json scan`);
|
|
15230
15582
|
entry = void 0;
|
|
15231
15583
|
}
|
|
15232
15584
|
if (entry && (!context.stackName || entry.producerStack !== context.stackName)) {
|
|
15233
15585
|
this.recordImport(context, exportName, entry.producerStack, entry.producerRegion);
|
|
15234
|
-
this.logger.info(`Resolved Fn::ImportValue: ${
|
|
15235
|
-
return await this.reresolveCrossStackValue(entry.value, entry.producerRegion, context, `Fn::ImportValue '${exportName}' (producer ${entry.producerStack} / ${entry.producerRegion})
|
|
15586
|
+
this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from index: ${entry.producerStack} / ${entry.producerRegion}; ${carriesDynamicReference(entry.value) ? "redacted dynamic reference" : "literal value"})`);
|
|
15587
|
+
return await this.reresolveCrossStackValue(entry.value, entry.producerRegion, context, `Fn::ImportValue '${exportName}' (producer ${entry.producerStack} / ${entry.producerRegion})`, sourceKey);
|
|
15236
15588
|
}
|
|
15237
15589
|
}
|
|
15238
15590
|
const allStacks = await context.stateBackend.listStacks();
|
|
15239
|
-
this.logger.debug(`Found ${allStacks.length} state record(s) to search for export: ${
|
|
15591
|
+
this.logger.debug(`Found ${allStacks.length} state record(s) to search for export: ${loggedExportName}`);
|
|
15240
15592
|
let found;
|
|
15241
15593
|
for (const ref of allStacks) {
|
|
15242
15594
|
const { stackName: refStack, region: refRegion } = ref;
|
|
@@ -15258,7 +15610,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
15258
15610
|
const { state } = stateData;
|
|
15259
15611
|
if (state.outputs && exportName in state.outputs) {
|
|
15260
15612
|
const value = state.outputs[exportName];
|
|
15261
|
-
this.logger.info(`Resolved Fn::ImportValue: ${
|
|
15613
|
+
this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from stack: ${refStack} / ${lookupRegion}; ${carriesDynamicReference(value) ? "redacted dynamic reference" : "literal value"})`);
|
|
15262
15614
|
if (context.exportIndex) context.exportIndex.patchEntry(exportName, {
|
|
15263
15615
|
value,
|
|
15264
15616
|
producerStack: refStack,
|
|
@@ -15279,11 +15631,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
15279
15631
|
continue;
|
|
15280
15632
|
}
|
|
15281
15633
|
}
|
|
15282
|
-
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);
|
|
15283
15635
|
if (this.cfnFallback) {
|
|
15284
|
-
const cfnExport = await this.lookupCfnExport(exportName);
|
|
15636
|
+
const cfnExport = await this.lookupCfnExport(exportName, context);
|
|
15285
15637
|
if (cfnExport) {
|
|
15286
|
-
this.logger.info(`Resolved Fn::ImportValue: ${
|
|
15638
|
+
this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from CloudFormation exports${cfnExport.exportingStackId ? `; exporting stack: ${cfnExport.exportingStackId}` : ""}; weak reference — producer is not cdkd-managed)`);
|
|
15287
15639
|
return cfnExport.value;
|
|
15288
15640
|
}
|
|
15289
15641
|
}
|
|
@@ -15301,7 +15653,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
15301
15653
|
* deliberate: without this fallback the deploy would have failed with
|
|
15302
15654
|
* the same not-found error anyway.
|
|
15303
15655
|
*/
|
|
15304
|
-
async lookupCfnExport(exportName) {
|
|
15656
|
+
async lookupCfnExport(exportName, context) {
|
|
15305
15657
|
let listing = this.cfnExportsPromise;
|
|
15306
15658
|
if (!listing) {
|
|
15307
15659
|
listing = this.fetchAllCfnExports();
|
|
@@ -15318,10 +15670,40 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
15318
15670
|
};
|
|
15319
15671
|
return;
|
|
15320
15672
|
} catch (error) {
|
|
15321
|
-
this.logger.warn(`Fn::ImportValue: CloudFormation ListExports fallback failed for export '${exportName}' (region ${this.resolverRegion}): ${error instanceof Error ? error.message : String(error)}. Grant cloudformation:ListExports to resolve exports from CloudFormation-managed stacks, or pass --no-cfn-fallback to disable the fallback.`);
|
|
15673
|
+
this.logger.warn(`Fn::ImportValue: CloudFormation ListExports fallback failed for export '${this.maskSecretsForLog(exportName, context)}' (region ${this.resolverRegion}): ${error instanceof Error ? error.message : String(error)}. Grant cloudformation:ListExports to resolve exports from CloudFormation-managed stacks, or pass --no-cfn-fallback to disable the fallback.`);
|
|
15322
15674
|
return;
|
|
15323
15675
|
}
|
|
15324
15676
|
}
|
|
15677
|
+
/**
|
|
15678
|
+
* Render the `Available outputs: ...` tail of an `Fn::GetStackOutput`
|
|
15679
|
+
* not-found error (issue #2133 review).
|
|
15680
|
+
*
|
|
15681
|
+
* These are the PRODUCER's `state.outputs` / CloudFormation output KEYS, and
|
|
15682
|
+
* they land in a top-level ERROR — the one thing on this path that reaches a
|
|
15683
|
+
* CI log at default verbosity. A key can itself hold plaintext: that is the
|
|
15684
|
+
* `secretBearingStateKeyWarning` class (issue #1919), which `cdkd scrub`
|
|
15685
|
+
* counts and deliberately never prints, so the enumeration must not be the
|
|
15686
|
+
* one place that does.
|
|
15687
|
+
*
|
|
15688
|
+
* MASKED and CAPPED rather than dropped. Masking is the treatment every other
|
|
15689
|
+
* identifier on this path already gets, and the cap bounds what one error can
|
|
15690
|
+
* disclose (a producer with hundreds of outputs would otherwise dump all of
|
|
15691
|
+
* them). Dropping the names entirely was considered and rejected: a typo'd
|
|
15692
|
+
* `OutputName` is the overwhelmingly common cause, and the list is what makes
|
|
15693
|
+
* the error actionable.
|
|
15694
|
+
*
|
|
15695
|
+
* Residual, stated rather than hidden: the needles belong to the CONSUMER's
|
|
15696
|
+
* resolution, so a plaintext sitting in a PRODUCER key that this consumer
|
|
15697
|
+
* never resolved is not maskable from here. The cap is what bounds that case;
|
|
15698
|
+
* `cdkd scrub` reporting the producer's own `secretBearingKeys` is the remedy.
|
|
15699
|
+
*/
|
|
15700
|
+
describeAvailableOutputs(keys, context) {
|
|
15701
|
+
if (keys.length === 0) return "(none)";
|
|
15702
|
+
const shown = keys.slice(0, MAX_LISTED_AVAILABLE_OUTPUTS);
|
|
15703
|
+
const rendered = shown.map((k) => this.maskSecretsForLog(k, context)).join(", ");
|
|
15704
|
+
const hidden = keys.length - shown.length;
|
|
15705
|
+
return hidden > 0 ? `${rendered} (+${hidden} more)` : rendered;
|
|
15706
|
+
}
|
|
15325
15707
|
/** Full paginated ListExports walk backing {@link lookupCfnExport}'s memo. */
|
|
15326
15708
|
async fetchAllCfnExports() {
|
|
15327
15709
|
const client = this.getCfnClient(this.resolverRegion);
|
|
@@ -15344,7 +15726,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
15344
15726
|
* logged for non-not-found failures). Same graceful-degradation
|
|
15345
15727
|
* contract as {@link lookupCfnExport}.
|
|
15346
15728
|
*/
|
|
15347
|
-
async lookupCfnStackOutputs(stackName, region) {
|
|
15729
|
+
async lookupCfnStackOutputs(stackName, region, context) {
|
|
15348
15730
|
const cacheKey = `${region}\0${stackName}`;
|
|
15349
15731
|
let fetch = this.cfnStackOutputsCache.get(cacheKey);
|
|
15350
15732
|
if (!fetch) {
|
|
@@ -15358,7 +15740,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
15358
15740
|
return await fetch;
|
|
15359
15741
|
} catch (error) {
|
|
15360
15742
|
const message = error instanceof Error ? error.message : String(error);
|
|
15361
|
-
this.logger.warn(`Fn::GetStackOutput: CloudFormation DescribeStacks fallback failed for stack '${stackName}' (${region}): ${message}. Grant cloudformation:DescribeStacks to resolve outputs from CloudFormation-managed stacks, or pass --no-cfn-fallback to disable the fallback.`);
|
|
15743
|
+
this.logger.warn(`Fn::GetStackOutput: CloudFormation DescribeStacks fallback failed for stack '${this.maskSecretsForLog(stackName, context)}' (${region}): ${message}. Grant cloudformation:DescribeStacks to resolve outputs from CloudFormation-managed stacks, or pass --no-cfn-fallback to disable the fallback.`);
|
|
15362
15744
|
return;
|
|
15363
15745
|
}
|
|
15364
15746
|
}
|
|
@@ -15454,6 +15836,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
15454
15836
|
const args = arg;
|
|
15455
15837
|
if (!("StackName" in args)) throw new Error("Fn::GetStackOutput: StackName is required");
|
|
15456
15838
|
if (!("OutputName" in args)) throw new Error("Fn::GetStackOutput: OutputName is required");
|
|
15839
|
+
const sourceKey = crossStackSourceKey({ "Fn::GetStackOutput": args });
|
|
15457
15840
|
const stackName = await this.resolveValue(args["StackName"], context);
|
|
15458
15841
|
if (typeof stackName !== "string" || stackName === "") throw new Error(`Fn::GetStackOutput: StackName must resolve to a non-empty string, got ${typeof stackName}`);
|
|
15459
15842
|
const outputName = await this.resolveValue(args["OutputName"], context);
|
|
@@ -15473,18 +15856,20 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
15473
15856
|
roleArn = raw;
|
|
15474
15857
|
}
|
|
15475
15858
|
if (!roleArn && context.stackName && context.stackName === stackName && region === this.resolverRegion) throw new Error(`Fn::GetStackOutput: cannot reference own stack '${stackName}' in the same region '${region}'`);
|
|
15476
|
-
this.
|
|
15859
|
+
const loggedStackName = this.maskSecretsForLog(stackName, context);
|
|
15860
|
+
const loggedOutputName = this.maskSecretsForLog(outputName, context);
|
|
15861
|
+
this.logger.debug(`Resolving Fn::GetStackOutput: StackName=${loggedStackName}, Region=${region}, OutputName=${loggedOutputName}${roleArn ? `, RoleArn=${roleArn}` : ""}`);
|
|
15477
15862
|
const stateData = roleArn ? await this.getCrossAccountStackState(roleArn, stackName, region, context) : await this.getSameAccountStackState(stackName, region, context);
|
|
15478
15863
|
if (!stateData) {
|
|
15479
15864
|
if (!roleArn && this.cfnFallback) {
|
|
15480
|
-
const cfnOutputs = await this.lookupCfnStackOutputs(stackName, region);
|
|
15865
|
+
const cfnOutputs = await this.lookupCfnStackOutputs(stackName, region, context);
|
|
15481
15866
|
if (cfnOutputs) {
|
|
15482
15867
|
if (!(outputName in cfnOutputs)) {
|
|
15483
|
-
const available = Object.keys(cfnOutputs)
|
|
15868
|
+
const available = this.describeAvailableOutputs(Object.keys(cfnOutputs), context);
|
|
15484
15869
|
throw new Error(`Fn::GetStackOutput: output '${outputName}' not found in CloudFormation stack '${stackName}' (${region}). Available outputs: ${available}`);
|
|
15485
15870
|
}
|
|
15486
15871
|
const value = cfnOutputs[outputName];
|
|
15487
|
-
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${
|
|
15872
|
+
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${region}, OutputName=${loggedOutputName} (from CloudFormation stack outputs; weak reference — producer is not cdkd-managed)`);
|
|
15488
15873
|
return value;
|
|
15489
15874
|
}
|
|
15490
15875
|
}
|
|
@@ -15492,14 +15877,14 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
15492
15877
|
}
|
|
15493
15878
|
const outputs = stateData.state.outputs ?? {};
|
|
15494
15879
|
if (!(outputName in outputs)) {
|
|
15495
|
-
const available = Object.keys(outputs)
|
|
15880
|
+
const available = this.describeAvailableOutputs(Object.keys(outputs), context);
|
|
15496
15881
|
throw new Error(`Fn::GetStackOutput: output '${outputName}' not found in stack '${stackName}' (${region}). Available outputs: ${available}`);
|
|
15497
15882
|
}
|
|
15498
15883
|
const value = outputs[outputName];
|
|
15499
|
-
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${
|
|
15884
|
+
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${region}, OutputName=${loggedOutputName}${roleArn ? `, RoleArn=${roleArn}` : ""} (${carriesDynamicReference(value) ? "redacted dynamic reference" : "literal value"})`);
|
|
15500
15885
|
if (!roleArn) this.recordOutputRead(context, stackName, region, outputName);
|
|
15501
|
-
if (roleArn && !context.skipDynamicReferences && carriesDynamicReference(value)) throw markNonRetryable(new
|
|
15502
|
-
return await this.reresolveCrossStackValue(value, region, context, `Fn::GetStackOutput '${outputName}' (producer ${stackName} / ${region})
|
|
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.`));
|
|
15887
|
+
return await this.reresolveCrossStackValue(value, region, context, `Fn::GetStackOutput '${outputName}' (producer ${stackName} / ${region})`, sourceKey);
|
|
15503
15888
|
}
|
|
15504
15889
|
/**
|
|
15505
15890
|
* Push a resolved `Fn::GetStackOutput` into the consumer's
|
|
@@ -16888,7 +17273,7 @@ var CloudControlProvider = class {
|
|
|
16888
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);
|
|
16889
17274
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
16890
17275
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
16891
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
17276
|
+
const { ASGProvider } = await import("./asg-provider-CZc9Mtx2.js").then((n) => n.n);
|
|
16892
17277
|
return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
|
|
16893
17278
|
}
|
|
16894
17279
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -25801,7 +26186,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
25801
26186
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
25802
26187
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
25803
26188
|
function getCdkdVersion() {
|
|
25804
|
-
return "0.284.
|
|
26189
|
+
return "0.284.33";
|
|
25805
26190
|
}
|
|
25806
26191
|
/**
|
|
25807
26192
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -28421,5 +28806,5 @@ var DeployEngine = class {
|
|
|
28421
28806
|
};
|
|
28422
28807
|
|
|
28423
28808
|
//#endregion
|
|
28424
|
-
export { endCommandInterruptScope as $,
|
|
28425
|
-
//# sourceMappingURL=deploy-engine-
|
|
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 };
|
|
28810
|
+
//# sourceMappingURL=deploy-engine-mhGFd83I.js.map
|