@go-to-k/cdkd 0.283.30 → 0.283.32
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-BpVWOeU3.js → asg-provider-CVoisQ0L.js} +2 -2
- package/dist/{asg-provider-BpVWOeU3.js.map → asg-provider-CVoisQ0L.js.map} +1 -1
- package/dist/cli.js +107 -27
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-Bb4ECtz3.js → deploy-engine-rYTNzst0.js} +576 -53
- package/dist/deploy-engine-rYTNzst0.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-Bb4ECtz3.js.map +0 -1
|
@@ -5560,6 +5560,27 @@ var AssetModeResolver = class {
|
|
|
5560
5560
|
function escapeRegExp$1(value) {
|
|
5561
5561
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5562
5562
|
}
|
|
5563
|
+
/**
|
|
5564
|
+
* Remove control characters from a value about to be PRINTED.
|
|
5565
|
+
*
|
|
5566
|
+
* Lives beside {@link escapeRegExp} for the reason that helper's own note
|
|
5567
|
+
* gives: this was spelled twice — in `src/cli/commands/diff-recursive.ts` and
|
|
5568
|
+
* in `src/deployment/outputs-export-alias.ts` — and copies of a
|
|
5569
|
+
* security-shaped helper drift. Both print the same class of string: a
|
|
5570
|
+
* resolved CloudFormation Output / `Export.Name`, which passed no CFn
|
|
5571
|
+
* validator, so it can carry ANSI escapes or bidi overrides straight into a
|
|
5572
|
+
* terminal or a CI log.
|
|
5573
|
+
*
|
|
5574
|
+
* C0 + DEL + C1 + the bidi marks and isolates. Apply on HUMAN-render paths
|
|
5575
|
+
* only: a `--json` payload is a machine interface where an export name is data
|
|
5576
|
+
* a consumer may match on, and mutating it there trades a correctness
|
|
5577
|
+
* regression for a display concern (see `stripDisplayOnlyChars`, which is the
|
|
5578
|
+
* narrower guard for an already-serialized payload and stays local to its
|
|
5579
|
+
* caller because its C0 exclusion is specific to that path).
|
|
5580
|
+
*/
|
|
5581
|
+
function stripControlChars(value) {
|
|
5582
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
|
|
5583
|
+
}
|
|
5563
5584
|
|
|
5564
5585
|
//#endregion
|
|
5565
5586
|
//#region src/assets/asset-redirect.ts
|
|
@@ -9925,40 +9946,88 @@ function buildNeedleRegex(values) {
|
|
|
9925
9946
|
if (needles.length === 0) return void 0;
|
|
9926
9947
|
return new RegExp(needles.map(escapeRegExp).join("|"), "g");
|
|
9927
9948
|
}
|
|
9928
|
-
/**
|
|
9949
|
+
/**
|
|
9950
|
+
* The bag was produced by resolving the source: same shape, template source.
|
|
9951
|
+
*
|
|
9952
|
+
* `sourceIsSameGeneration` is FALSE despite the name, and that is the whole
|
|
9953
|
+
* lesson of issue #1917's review: the RULES describe the source, but the deploy
|
|
9954
|
+
* engine's persist choke point applies them to every record in the state map,
|
|
9955
|
+
* including ones this pass never rewrote. A template can never certify the
|
|
9956
|
+
* generation of the bag it is walked against.
|
|
9957
|
+
*/
|
|
9929
9958
|
const TEMPLATE_DERIVED_RULES = {
|
|
9930
9959
|
descendArrays: true,
|
|
9931
|
-
trustAnyExpression: false
|
|
9960
|
+
trustAnyExpression: false,
|
|
9961
|
+
sourceIsSameGeneration: false
|
|
9962
|
+
};
|
|
9963
|
+
/**
|
|
9964
|
+
* An AWS readback, or a persisted STATE bag, projected from the TEMPLATE: no
|
|
9965
|
+
* relaxation applies.
|
|
9966
|
+
*
|
|
9967
|
+
* One constant covers both bags because the template is what decides all three
|
|
9968
|
+
* rules here, and it decides them identically: shapes may diverge (no
|
|
9969
|
+
* positional descent), the template carries PUBLIC ssm expressions (no blanket
|
|
9970
|
+
* trust), and it is a different generation from anything it is walked against
|
|
9971
|
+
* (no source-takes-precedence on an expression-shaped leaf). An earlier draft
|
|
9972
|
+
* split this into a second `TEMPLATE_SOURCED_STATE_BAG_RULES` for `cdkd scrub`;
|
|
9973
|
+
* the two ended up byte-identical once the axis moved from CALLER to
|
|
9974
|
+
* GENERATION, and two identical constants are a drift hazard, not a
|
|
9975
|
+
* distinction.
|
|
9976
|
+
*/
|
|
9977
|
+
const TEMPLATE_SOURCED_RULES = {
|
|
9978
|
+
descendArrays: false,
|
|
9979
|
+
trustAnyExpression: false,
|
|
9980
|
+
sourceIsSameGeneration: false
|
|
9932
9981
|
};
|
|
9933
|
-
/** An AWS readback projected from
|
|
9982
|
+
/** An AWS readback projected from THIS record's own persisted STATE bag. */
|
|
9934
9983
|
const STATE_SOURCED_READBACK_RULES = {
|
|
9935
9984
|
descendArrays: false,
|
|
9936
|
-
trustAnyExpression: true
|
|
9985
|
+
trustAnyExpression: true,
|
|
9986
|
+
sourceIsSameGeneration: true
|
|
9937
9987
|
};
|
|
9938
|
-
/**
|
|
9939
|
-
|
|
9988
|
+
/**
|
|
9989
|
+
* A STATE source that is no longer this bag's own generation — `cdkd scrub`'s
|
|
9990
|
+
* `observedProperties` walk (issue #1917 review).
|
|
9991
|
+
*
|
|
9992
|
+
* It differs from {@link STATE_SOURCED_READBACK_RULES} on one flag, and the
|
|
9993
|
+
* difference is not cosmetic. Scrub positions `properties` against TODAY's
|
|
9994
|
+
* template BEFORE the observed bag is walked, so the "record's own properties"
|
|
9995
|
+
* that serve as the observed source may already carry an expression the stack
|
|
9996
|
+
* has never deployed. Taking that source for an observed leaf that ALREADY
|
|
9997
|
+
* holds an expression rewrites the drift baseline onto an undeployed
|
|
9998
|
+
* reference — which `cdkd drift --revert` then pushes to AWS.
|
|
9999
|
+
*
|
|
10000
|
+
* `trustAnyExpression` stays TRUE: the source is still a STATE bag, holding no
|
|
10001
|
+
* public expressions, and that relaxation is what lets scrub clean a legacy
|
|
10002
|
+
* PLAINTEXT observed leaf (issue #1900) rather than falling back to a value
|
|
10003
|
+
* scan that an old state file gives no needles for.
|
|
10004
|
+
*/
|
|
10005
|
+
const STATE_SOURCED_CROSS_GENERATION_RULES = {
|
|
9940
10006
|
descendArrays: false,
|
|
9941
|
-
trustAnyExpression:
|
|
10007
|
+
trustAnyExpression: true,
|
|
10008
|
+
sourceIsSameGeneration: false
|
|
9942
10009
|
};
|
|
9943
10010
|
/**
|
|
9944
|
-
* The bag was produced by resolving a STATE source —
|
|
10011
|
+
* The bag was produced by resolving a STATE source — every relaxation applies.
|
|
9945
10012
|
*
|
|
9946
10013
|
* The rollback replay is the case (issue #1910): `resolveReplayProps` resolves
|
|
9947
10014
|
* the JOURNALED bag and the provider's `effectiveProperties` come back from
|
|
9948
10015
|
* that, so the two have identical structure and positional array descent is
|
|
9949
|
-
* sound exactly as it is for a template-derived bag.
|
|
9950
|
-
*
|
|
9951
|
-
*
|
|
9952
|
-
*
|
|
10016
|
+
* sound exactly as it is for a template-derived bag. The source is a persisted
|
|
10017
|
+
* record, which holds no PUBLIC expressions — a `String` ssm reference is
|
|
10018
|
+
* stored resolved — so any `{{resolve:...}}` in it is by construction a secret.
|
|
10019
|
+
* And it is the SAME generation the bag was resolved from, one statement
|
|
10020
|
+
* earlier in the same call, which is as strong as that claim ever gets.
|
|
9953
10021
|
*
|
|
9954
|
-
* Using
|
|
9955
|
-
*
|
|
9956
|
-
*
|
|
9957
|
-
*
|
|
10022
|
+
* Using a `*_SOURCED_*` constant here instead would be wrong in the quiet
|
|
10023
|
+
* direction: it turns positional array descent OFF for a bag that genuinely
|
|
10024
|
+
* does correspond positionally, and drops the generation claim for the one
|
|
10025
|
+
* writer that can actually make it.
|
|
9958
10026
|
*/
|
|
9959
10027
|
const STATE_DERIVED_RULES = {
|
|
9960
10028
|
descendArrays: true,
|
|
9961
|
-
trustAnyExpression: true
|
|
10029
|
+
trustAnyExpression: true,
|
|
10030
|
+
sourceIsSameGeneration: true
|
|
9962
10031
|
};
|
|
9963
10032
|
function isDynamicReferenceString(value) {
|
|
9964
10033
|
return typeof value === "string" && value.includes("{{resolve:");
|
|
@@ -10252,6 +10321,103 @@ function positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions) {
|
|
|
10252
10321
|
return matched;
|
|
10253
10322
|
}
|
|
10254
10323
|
/**
|
|
10324
|
+
* Keys tried, in order, when pairing two arrays whose ORDER cannot be trusted
|
|
10325
|
+
* (issue #1915).
|
|
10326
|
+
*
|
|
10327
|
+
* Both are the identity field of an unordered CloudFormation list that really
|
|
10328
|
+
* does carry secrets: `Name` is the ECS `ContainerDefinitions[]` /
|
|
10329
|
+
* `Environment[]` / `Secrets[]` and CodeBuild `EnvironmentVariables[]` shape,
|
|
10330
|
+
* `Key` is the `Tags[]` shape the drift normalizer already keys on
|
|
10331
|
+
* (`canonicalizeTagListsDeep` in `src/analyzer/drift-normalize.ts`). The list
|
|
10332
|
+
* is deliberately SHORT: an entry is only useful when it is a genuine identity,
|
|
10333
|
+
* and a wrong entry costs a refused pairing rather than a wrong one (see
|
|
10334
|
+
* {@link identityKeyFor}), so widening it buys little and has to be justified
|
|
10335
|
+
* per shape.
|
|
10336
|
+
*
|
|
10337
|
+
* Worth naming rather than leaving to be rediscovered: `drift-normalize.ts`
|
|
10338
|
+
* deliberately moved its GENERAL case OFF heuristics like this one, onto
|
|
10339
|
+
* provider-DECLARED paths (`getDriftUnorderedPaths` + `matchesPathPrefix`),
|
|
10340
|
+
* after issue #1783 showed a heuristic claiming order-independence for a list
|
|
10341
|
+
* that was order-SIGNIFICANT (`KeySchema`). Only its `Tags[]` pass is still a
|
|
10342
|
+
* heuristic. This module diverges on purpose, because the two failure modes are
|
|
10343
|
+
* not comparable: there, a wrong claim SUPPRESSES real drift; here, the worst a
|
|
10344
|
+
* wrong pairing key can do is fail the uniqueness test and refuse, leaving the
|
|
10345
|
+
* leaf exactly where the value scan already had it. If that ever stops being
|
|
10346
|
+
* true — if a pairing gains the power to DELETE or REORDER rather than only to
|
|
10347
|
+
* rewrite a matched leaf — this list has to move to a declared seam too.
|
|
10348
|
+
*/
|
|
10349
|
+
const ARRAY_IDENTITY_KEYS = ["Name", "Key"];
|
|
10350
|
+
/**
|
|
10351
|
+
* Is every element of `items` a plain object carrying a NON-EMPTY OWN string at
|
|
10352
|
+
* `key`, with no two elements sharing a value?
|
|
10353
|
+
*
|
|
10354
|
+
* `Object.hasOwn` for the same reason the object walk below uses it: without
|
|
10355
|
+
* it the prototype chain answers for a key like `constructor`, and while state
|
|
10356
|
+
* that came from `JSON.parse` cannot carry one, this module is called with
|
|
10357
|
+
* caller-constructed bags too and the two walks disagreeing is the kind of gap
|
|
10358
|
+
* that only shows up once something else changes.
|
|
10359
|
+
*
|
|
10360
|
+
* The empty string is excluded because it is not a distinguishing identity —
|
|
10361
|
+
* the same reason the value scan refuses it as a needle. Two elements both
|
|
10362
|
+
* carrying `Name: ''` would otherwise pair on "equality" that means nothing.
|
|
10363
|
+
*/
|
|
10364
|
+
function isUniquelyKeyedBy(items, key) {
|
|
10365
|
+
if (items.length === 0) return false;
|
|
10366
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10367
|
+
for (const item of items) {
|
|
10368
|
+
if (!isPlainObject$2(item) || !Object.hasOwn(item, key)) return false;
|
|
10369
|
+
const id = item[key];
|
|
10370
|
+
if (typeof id !== "string" || id === "") return false;
|
|
10371
|
+
if (seen.has(id)) return false;
|
|
10372
|
+
seen.add(id);
|
|
10373
|
+
}
|
|
10374
|
+
return true;
|
|
10375
|
+
}
|
|
10376
|
+
/**
|
|
10377
|
+
* Pick a key that identifies the elements of BOTH arrays, or `undefined` when
|
|
10378
|
+
* none does (issue #1915).
|
|
10379
|
+
*
|
|
10380
|
+
* This is what lets a leaf nested in an ARRAY be positioned on a path where
|
|
10381
|
+
* positional descent is refused. `descendArrays: false` is not a claim that the
|
|
10382
|
+
* bag and source elements are unrelated — it is a claim that their ORDER does
|
|
10383
|
+
* not correspond, because AWS does not preserve list order (the reason
|
|
10384
|
+
* `src/analyzer/drift-normalize.ts` exists). Matching by an identity FIELD
|
|
10385
|
+
* answers the order objection directly instead of working around it, so the
|
|
10386
|
+
* relaxation is sound on every rules constant and is deliberately NOT gated on
|
|
10387
|
+
* one.
|
|
10388
|
+
*
|
|
10389
|
+
* Requiring uniqueness WITHIN each array is what makes a pairing impossible to
|
|
10390
|
+
* get wrong: pairing is by string EQUALITY, so a bag element can only ever meet
|
|
10391
|
+
* the source element carrying the same identity, and uniqueness means there is
|
|
10392
|
+
* at most one of those. A key that is not really an identity (a repeated enum
|
|
10393
|
+
* value) fails the uniqueness test and refuses the whole pairing rather than
|
|
10394
|
+
* producing a plausible-looking wrong one. An element with no partner is not
|
|
10395
|
+
* guessed at either — it falls to the value scan, exactly as the whole array
|
|
10396
|
+
* did before.
|
|
10397
|
+
*
|
|
10398
|
+
* Keys are tried in {@link ARRAY_IDENTITY_KEYS} order and the FIRST one that
|
|
10399
|
+
* qualifies on both sides wins; a key that fails does not veto the next one.
|
|
10400
|
+
*
|
|
10401
|
+
* Two shapes this deliberately does NOT reach, stated so they are bounds rather
|
|
10402
|
+
* than surprises — both fail closed, leaving the leaf to the value scan:
|
|
10403
|
+
*
|
|
10404
|
+
* - **The identity field itself holds a secret.** The source element carries
|
|
10405
|
+
* `{Name: '{{resolve:...}}'}` and the bag element the resolved plaintext, so
|
|
10406
|
+
* the two never pair and the element's other leaves are not reached either.
|
|
10407
|
+
* Closing it would mean pairing on a field whose two sides are KNOWN to
|
|
10408
|
+
* differ, i.e. exactly the guessing the uniqueness rule exists to forbid. On
|
|
10409
|
+
* the unchanged-resource path the value scan is also a no-op, so such a leaf
|
|
10410
|
+
* keeps its plaintext — narrow (an identity field is a name, not a
|
|
10411
|
+
* credential) but real.
|
|
10412
|
+
* - **Arrays of arrays.** `M: [[{Name, Value}]]` pairs nothing, because the
|
|
10413
|
+
* OUTER elements are arrays rather than plain objects and have no identity
|
|
10414
|
+
* field to key on. Positional descent into the outer list would reintroduce
|
|
10415
|
+
* the order assumption this function exists to avoid.
|
|
10416
|
+
*/
|
|
10417
|
+
function identityKeyFor(bag, source) {
|
|
10418
|
+
for (const key of ARRAY_IDENTITY_KEYS) if (isUniquelyKeyedBy(bag, key) && isUniquelyKeyedBy(source, key)) return key;
|
|
10419
|
+
}
|
|
10420
|
+
/**
|
|
10255
10421
|
* PATH-based redaction: walk `bag` alongside a SOURCE bag that still carries the
|
|
10256
10422
|
* unresolved `{{resolve:...}}` expressions, and wherever the source leaf is such
|
|
10257
10423
|
* a string, persist THAT string verbatim.
|
|
@@ -10284,16 +10450,41 @@ function positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions) {
|
|
|
10284
10450
|
* where neither is.
|
|
10285
10451
|
*/
|
|
10286
10452
|
function redactByPath(bag, source, secrets, rules, secretExpressions) {
|
|
10287
|
-
if (typeof bag === "string" && isSingleDynamicReferenceToken(bag)) return bag;
|
|
10288
10453
|
if (isDynamicReferenceString(source) && typeof bag === "string") {
|
|
10289
|
-
if (isSingleDynamicReferenceToken(source) && (rules.trustAnyExpression || isKnownSecretExpression(source, secretExpressions)))
|
|
10454
|
+
if (isSingleDynamicReferenceToken(source) && (rules.trustAnyExpression || isKnownSecretExpression(source, secretExpressions))) {
|
|
10455
|
+
if (!rules.sourceIsSameGeneration && isSingleDynamicReferenceToken(bag)) return secrets.get(bag) ?? bag;
|
|
10456
|
+
return source;
|
|
10457
|
+
}
|
|
10290
10458
|
return redactSecretsForState(bag, secrets);
|
|
10291
10459
|
}
|
|
10292
10460
|
if (typeof bag === "string" && isPlainObject$2(source)) {
|
|
10293
10461
|
const positioned = positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions);
|
|
10294
10462
|
if (positioned !== void 0) return positioned;
|
|
10295
10463
|
}
|
|
10296
|
-
if (
|
|
10464
|
+
if (Array.isArray(bag) && Array.isArray(source)) {
|
|
10465
|
+
const key = identityKeyFor(bag, source);
|
|
10466
|
+
if (key !== void 0) {
|
|
10467
|
+
const sourceIndexById = /* @__PURE__ */ new Map();
|
|
10468
|
+
source.forEach((item, i) => {
|
|
10469
|
+
sourceIndexById.set(item[key], i);
|
|
10470
|
+
});
|
|
10471
|
+
let orderPreserved = true;
|
|
10472
|
+
const partnerIndex = bag.map((item, i) => {
|
|
10473
|
+
const j = sourceIndexById.get(item[key]);
|
|
10474
|
+
if (j === void 0) return -1;
|
|
10475
|
+
if (j !== i) orderPreserved = false;
|
|
10476
|
+
return j;
|
|
10477
|
+
});
|
|
10478
|
+
const positionalIsExact = rules.descendArrays && bag.length === source.length && orderPreserved;
|
|
10479
|
+
return bag.map((item, i) => {
|
|
10480
|
+
const j = partnerIndex[i];
|
|
10481
|
+
if (j >= 0) return redactByPath(item, source[j], secrets, rules, secretExpressions);
|
|
10482
|
+
if (positionalIsExact) return redactByPath(item, source[i], secrets, rules, secretExpressions);
|
|
10483
|
+
return redactSecretsForState(item, secrets);
|
|
10484
|
+
});
|
|
10485
|
+
}
|
|
10486
|
+
if (rules.descendArrays && bag.length === source.length) return bag.map((item, i) => redactByPath(item, source[i], secrets, rules, secretExpressions));
|
|
10487
|
+
}
|
|
10297
10488
|
if (isPlainObject$2(bag) && isPlainObject$2(source)) {
|
|
10298
10489
|
const out = {};
|
|
10299
10490
|
for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? redactByPath(v, source[k], secrets, rules, secretExpressions) : redactSecretsForState(v, secrets);
|
|
@@ -10319,6 +10510,7 @@ function redactSecretsForState(bag, secrets, source, rules = TEMPLATE_DERIVED_RU
|
|
|
10319
10510
|
if (typeof value === "string") {
|
|
10320
10511
|
const whole = wholeValueExpr(value);
|
|
10321
10512
|
if (whole !== void 0) return whole;
|
|
10513
|
+
if (isSingleDynamicReferenceToken(value)) return value;
|
|
10322
10514
|
if (!regex) return value;
|
|
10323
10515
|
regex.lastIndex = 0;
|
|
10324
10516
|
if (!regex.test(value)) return value;
|
|
@@ -10342,13 +10534,23 @@ function redactSecretsForState(bag, secrets, source, rules = TEMPLATE_DERIVED_RU
|
|
|
10342
10534
|
* changed, or the input by identity when there are no secrets — so callers can
|
|
10343
10535
|
* detect a no-op cheaply. Shared by the deploy engine's save choke point and
|
|
10344
10536
|
* the `cdkd scrub` command so both scrub the same three fields identically.
|
|
10345
|
-
|
|
10346
|
-
|
|
10537
|
+
*
|
|
10538
|
+
* `observedRules` overrides the rules the `observedProperties` walk would
|
|
10539
|
+
* otherwise DERIVE from whether a `sourceProperties` bag was supplied. Exactly
|
|
10540
|
+
* one caller needs it and the reason is not obvious, so it is a parameter
|
|
10541
|
+
* rather than another derivation: `cdkd scrub` has already repositioned
|
|
10542
|
+
* `properties` against TODAY's template by the time it calls this, so the
|
|
10543
|
+
* "record's own properties" this walk falls back to are no longer the same
|
|
10544
|
+
* GENERATION as the observed bag beside them (issue #1917 review). Left
|
|
10545
|
+
* unspecified, the derivation below is right for every other caller, whose
|
|
10546
|
+
* `properties` reach this function untouched.
|
|
10547
|
+
*/
|
|
10548
|
+
function scrubResourceRecord(record, secrets, sourceProperties, observedRules) {
|
|
10347
10549
|
if (secrets.size === 0 && sourceProperties === void 0 && !record.observedProperties) return record;
|
|
10348
10550
|
const next = { ...record };
|
|
10349
10551
|
next.properties = redactSecretsForState(record.properties, secrets, sourceProperties);
|
|
10350
10552
|
if (record.attributes) next.attributes = redactSecretsForState(record.attributes, secrets);
|
|
10351
|
-
if (record.observedProperties) next.observedProperties = redactSecretsForState(record.observedProperties, secrets, sourceProperties ?? next.properties, sourceProperties === void 0 ? STATE_SOURCED_READBACK_RULES :
|
|
10553
|
+
if (record.observedProperties) next.observedProperties = redactSecretsForState(record.observedProperties, secrets, sourceProperties ?? next.properties, observedRules ?? (sourceProperties === void 0 ? STATE_SOURCED_READBACK_RULES : TEMPLATE_SOURCED_RULES));
|
|
10352
10554
|
return next;
|
|
10353
10555
|
}
|
|
10354
10556
|
/**
|
|
@@ -14895,7 +15097,7 @@ var CloudControlProvider = class {
|
|
|
14895
15097
|
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);
|
|
14896
15098
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
14897
15099
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
14898
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
15100
|
+
const { ASGProvider } = await import("./asg-provider-CVoisQ0L.js").then((n) => n.n);
|
|
14899
15101
|
return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
|
|
14900
15102
|
}
|
|
14901
15103
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -19950,6 +20152,262 @@ function collectInlinePolicyNamesManagedBySiblings(targetPhysicalId, context, at
|
|
|
19950
20152
|
return result;
|
|
19951
20153
|
}
|
|
19952
20154
|
|
|
20155
|
+
//#endregion
|
|
20156
|
+
//#region src/deployment/outputs-export-alias.ts
|
|
20157
|
+
/**
|
|
20158
|
+
* Does CloudFormation suppress this output on this deploy?
|
|
20159
|
+
*
|
|
20160
|
+
* CFn does not create an output whose `Condition` evaluates false, and
|
|
20161
|
+
* `resolveOutputs` mirrors that (issue #1028). Unknown condition names are
|
|
20162
|
+
* KEPT, matching `filterResourcesByCondition` on the resource side — a
|
|
20163
|
+
* condition cdkd could not evaluate must not silently delete an output.
|
|
20164
|
+
*
|
|
20165
|
+
* DEPLOY-SIDE ONLY. `cdkd scrub` deliberately does not use this: see
|
|
20166
|
+
* {@link collectDeclaredOutputNames}.
|
|
20167
|
+
*/
|
|
20168
|
+
function isOutputSuppressedByCondition(output, conditions) {
|
|
20169
|
+
return output.Condition !== void 0 && conditions?.[output.Condition] === false;
|
|
20170
|
+
}
|
|
20171
|
+
/**
|
|
20172
|
+
* The output NAMES this deploy actually publishes — every declared output minus
|
|
20173
|
+
* the condition-suppressed ones.
|
|
20174
|
+
*
|
|
20175
|
+
* This is the set that owns keys in BOTH bags, and the reason the answer is
|
|
20176
|
+
* "published" rather than "declared": a suppressed output writes no value, so
|
|
20177
|
+
* it must not write a position source either, and its name is free for an
|
|
20178
|
+
* export alias to use. Reserving names for suppressed outputs would drop a
|
|
20179
|
+
* WORKING export the moment an unrelated condition went false.
|
|
20180
|
+
*
|
|
20181
|
+
* Sound at deploy time because these are the SAME condition values the deploy
|
|
20182
|
+
* itself acted on. Not sound in scrub — see {@link collectDeclaredOutputNames}.
|
|
20183
|
+
*/
|
|
20184
|
+
function collectPublishedOutputNames(outputs, conditions) {
|
|
20185
|
+
const names = /* @__PURE__ */ new Set();
|
|
20186
|
+
for (const [name, output] of Object.entries(outputs)) if (!isOutputSuppressedByCondition(output, conditions)) names.add(name);
|
|
20187
|
+
return names;
|
|
20188
|
+
}
|
|
20189
|
+
/**
|
|
20190
|
+
* Every DECLARED output name, conditions ignored — the set `cdkd scrub` tests
|
|
20191
|
+
* collisions against.
|
|
20192
|
+
*
|
|
20193
|
+
* Scrub must be a SUPERSET here, and the asymmetry with the deploy engine is
|
|
20194
|
+
* forced by what scrub can know. Its condition values are re-evaluated
|
|
20195
|
+
* best-effort, from template defaults only (the command takes no
|
|
20196
|
+
* `--parameters`), and `evaluateConditions` assumes FALSE on any evaluation
|
|
20197
|
+
* failure. So "suppressed" is both easy to hit spuriously and impossible to
|
|
20198
|
+
* confirm against the deploy that actually wrote the state.
|
|
20199
|
+
*
|
|
20200
|
+
* The two error directions are not symmetric, which is what settles the rule:
|
|
20201
|
+
*
|
|
20202
|
+
* - Judging a colliding output suppressed when the DEPLOY published it (the
|
|
20203
|
+
* spurious-false case above) makes scrub miss the collision, write the
|
|
20204
|
+
* exporting output's expression over the colliding key, and persist a
|
|
20205
|
+
* reference naming a DIFFERENT secret — the #1919 corruption, produced by
|
|
20206
|
+
* the remediation command itself.
|
|
20207
|
+
* - Judging it published when the deploy suppressed it costs one spurious
|
|
20208
|
+
* warning and one key redacted by VALUE match instead of by position. State
|
|
20209
|
+
* exactly what that costs, since an earlier revision understated it: the
|
|
20210
|
+
* value map is keyed by PLAINTEXT, so when two DISTINCT secrets resolve to
|
|
20211
|
+
* one value it keeps only the last, and that key can be persisted holding a
|
|
20212
|
+
* reference naming the OTHER secret. It is a smaller blast radius than the
|
|
20213
|
+
* first case (one key, and only when two secrets coincide, versus every
|
|
20214
|
+
* collision) but it is the same KIND of error, not a mere loss of precision.
|
|
20215
|
+
*
|
|
20216
|
+
* A wrong reference beats a lost precision bound, so scrub over-approximates.
|
|
20217
|
+
*/
|
|
20218
|
+
function collectDeclaredOutputNames(outputs) {
|
|
20219
|
+
return new Set(Object.keys(outputs));
|
|
20220
|
+
}
|
|
20221
|
+
/**
|
|
20222
|
+
* Would aliasing `exportName` land on a key another output owns?
|
|
20223
|
+
*
|
|
20224
|
+
* An output exporting under its OWN name is not a collision: the alias rewrites
|
|
20225
|
+
* the identical key with the identical value, and both bags then carry the same
|
|
20226
|
+
* source.
|
|
20227
|
+
*
|
|
20228
|
+
* Deliberately NOT extended to two outputs sharing one `Export.Name` with no
|
|
20229
|
+
* output of that name. Both bags stay consistent there (one iteration writes
|
|
20230
|
+
* both the value and its source), so it is not this issue's class — see
|
|
20231
|
+
* `docs/cross-stack-references.md`.
|
|
20232
|
+
*/
|
|
20233
|
+
function isExportAliasCollision(exportName, outputKey, ownedOutputNames) {
|
|
20234
|
+
return exportName !== outputKey && ownedOutputNames.has(exportName);
|
|
20235
|
+
}
|
|
20236
|
+
/**
|
|
20237
|
+
* Mirrors `secret-redaction`'s own `MIN_NEEDLE_LENGTH`. Duplicated rather than
|
|
20238
|
+
* imported because the two bounds answer different questions and should be free
|
|
20239
|
+
* to diverge: that one bounds what may be REWRITTEN, this one what may be
|
|
20240
|
+
* REFUSED or REPORTED.
|
|
20241
|
+
*/
|
|
20242
|
+
const MIN_SECRET_NEEDLE = 4;
|
|
20243
|
+
/**
|
|
20244
|
+
* Which recorded secrets are visible in `text`, or `undefined` when none is.
|
|
20245
|
+
*
|
|
20246
|
+
* ONE rule for both callers below, because they were inconsistent and the
|
|
20247
|
+
* inconsistency was a hole: the export-name check refused only an exact
|
|
20248
|
+
* whole-name match while the state-KEY scan did bounded containment over the
|
|
20249
|
+
* same kind of map — so `prod-<secret>-endpoint` was written as a state key by
|
|
20250
|
+
* the deploy and then reported by `cdkd scrub` as an UNREPAIRABLE leak. The tool
|
|
20251
|
+
* was creating the exact state it tells the user it cannot fix.
|
|
20252
|
+
*
|
|
20253
|
+
* A WHOLE-value match counts at any length; an EMBEDDED one only at or above
|
|
20254
|
+
* {@link MIN_SECRET_NEEDLE}. The bound is what makes containment safe to use for
|
|
20255
|
+
* a refusal at all — an unbounded scan over a degenerate one-character secret
|
|
20256
|
+
* matches almost every name and silently drops working exports — and its cost
|
|
20257
|
+
* is stated rather than hidden: a secret of three characters or fewer embedded
|
|
20258
|
+
* in a longer string is not seen. That value is indistinguishable from
|
|
20259
|
+
* coincidence, and the same bound already governs every substring redaction
|
|
20260
|
+
* cdkd performs.
|
|
20261
|
+
*
|
|
20262
|
+
* No empty-string case is needed at either caller: the resolver never records an
|
|
20263
|
+
* empty secret (`secret-redaction` says so — an empty needle would match every
|
|
20264
|
+
* leaf), and the length bound excludes it from the containment arm regardless.
|
|
20265
|
+
*/
|
|
20266
|
+
function secretsPresentIn(text, secrets) {
|
|
20267
|
+
if (!secrets || secrets.size === 0) return void 0;
|
|
20268
|
+
const exposure = /* @__PURE__ */ new Map();
|
|
20269
|
+
for (const [plaintext, expression] of secrets) if (text === plaintext || plaintext.length >= MIN_SECRET_NEEDLE && text.includes(plaintext)) exposure.set(plaintext, expression);
|
|
20270
|
+
return exposure.size > 0 ? exposure : void 0;
|
|
20271
|
+
}
|
|
20272
|
+
/**
|
|
20273
|
+
* The secrets present in a resolved `Export.Name`, or `undefined` when it
|
|
20274
|
+
* carries none.
|
|
20275
|
+
*
|
|
20276
|
+
* An `Export.Name` may be an intrinsic (`Fn::Sub` / `Fn::Join`), and those
|
|
20277
|
+
* substitute dynamic references — so the resolved name can contain a resolved
|
|
20278
|
+
* secret. That name would become a state KEY, and every redaction pass walks
|
|
20279
|
+
* VALUES only, so the plaintext would land in `state.json` and be republished
|
|
20280
|
+
* into the exports index.
|
|
20281
|
+
*
|
|
20282
|
+
* TWO signals, and neither subsumes the other:
|
|
20283
|
+
*
|
|
20284
|
+
* - `substitutedIntoName` — the caller resolves the name with its OWN
|
|
20285
|
+
* `recordedSecretValues` map, so a non-empty map means the resolver
|
|
20286
|
+
* substituted a secret INTO THIS NAME. Exact, and the only arm that can see a
|
|
20287
|
+
* substituted secret SHORTER than the containment bound below.
|
|
20288
|
+
* - a bounded containment scan of `recordedThisPass` ({@link secretsPresentIn}),
|
|
20289
|
+
* which catches plaintext that arrived by any other route — a literal name, a
|
|
20290
|
+
* cache hit, an `Fn::Sub` variable echoing the value.
|
|
20291
|
+
*
|
|
20292
|
+
* An earlier revision had only the first, calling the scan unpromising because
|
|
20293
|
+
* an UNBOUNDED one is: a degenerate one-character recorded secret would make
|
|
20294
|
+
* every name containing that character "secret" and silently drop working
|
|
20295
|
+
* exports. The bound is what makes the scan safe, and without the scan the
|
|
20296
|
+
* deploy wrote `prod-<secret>-endpoint` as a state key that `cdkd scrub` then
|
|
20297
|
+
* reported as unrepairable.
|
|
20298
|
+
*
|
|
20299
|
+
* The containment arm applies with no order caveat any more: the deploy engine
|
|
20300
|
+
* resolves EVERY output value
|
|
20301
|
+
* before deciding any alias, so this arm sees the complete map whatever the
|
|
20302
|
+
* declaration order. (An earlier revision of this paragraph said catching that
|
|
20303
|
+
* required "resolving the whole template before deciding anything" and called
|
|
20304
|
+
* it impractical — the value pass already does exactly that, at no extra cost.
|
|
20305
|
+
* The residual is now only a secret first substituted by ANOTHER output's
|
|
20306
|
+
* `Export.Name`, since those resolve in the second pass, in declaration order.)
|
|
20307
|
+
*
|
|
20308
|
+
* No empty-string special case, in either map: the resolver never records an
|
|
20309
|
+
* empty secret (`secret-redaction.ts` states it — an empty needle would match
|
|
20310
|
+
* every leaf), so an `''` key cannot reach here, and a guard against it would
|
|
20311
|
+
* be a branch no test could ever distinguish.
|
|
20312
|
+
*/
|
|
20313
|
+
function exportNameSecretExposure(exportName, substitutedIntoName, recordedThisPass) {
|
|
20314
|
+
const exposure = new Map(substitutedIntoName);
|
|
20315
|
+
for (const [plaintext, expression] of secretsPresentIn(exportName, recordedThisPass) ?? []) exposure.set(plaintext, expression);
|
|
20316
|
+
return exposure.size > 0 ? exposure : void 0;
|
|
20317
|
+
}
|
|
20318
|
+
/**
|
|
20319
|
+
* Secrets visible in a state KEY, or `undefined` when there are none.
|
|
20320
|
+
*
|
|
20321
|
+
* Unlike {@link exportNameSecretExposure} there is no resolution to attribute
|
|
20322
|
+
* this to: the key was written by an EARLIER binary, so containment is the only
|
|
20323
|
+
* available signal. It is therefore bounded the same way `secret-redaction`
|
|
20324
|
+
* bounds its own substring scan — a value shorter than {@link MIN_SECRET_NEEDLE}
|
|
20325
|
+
* is matched only as the WHOLE key — because an unbounded containment scan over
|
|
20326
|
+
* a degenerate short secret flags every key in the state and fails the
|
|
20327
|
+
* `--dry-run --fail` CI gate repo-wide, which is the availability failure the
|
|
20328
|
+
* export-name check was redesigned to avoid.
|
|
20329
|
+
*
|
|
20330
|
+
* Bound and cost are {@link secretsPresentIn}'s; see there.
|
|
20331
|
+
*/
|
|
20332
|
+
function stateKeySecretExposure(key, secrets) {
|
|
20333
|
+
return secretsPresentIn(key, secrets);
|
|
20334
|
+
}
|
|
20335
|
+
/**
|
|
20336
|
+
* Replace EVERY occurrence of every exposed secret with the mask.
|
|
20337
|
+
*
|
|
20338
|
+
* Deliberately not `maskSecretsInText`: that helper skips needles shorter than
|
|
20339
|
+
* its minimum length, which is right when scanning arbitrary bags for
|
|
20340
|
+
* coincidental matches but wrong here, where the values are known to be IN this
|
|
20341
|
+
* string. Feeding a detected-but-unmaskable name to it printed the secret under
|
|
20342
|
+
* a "masked:" label — a message asserting a protection it had not performed.
|
|
20343
|
+
* Longest-first so a secret containing another is masked whole.
|
|
20344
|
+
*/
|
|
20345
|
+
function maskEveryOccurrence(text, exposure) {
|
|
20346
|
+
let out = text;
|
|
20347
|
+
for (const value of Array.from(exposure.keys()).sort((a, b) => b.length - a.length)) out = out.split(value).join("***");
|
|
20348
|
+
return out;
|
|
20349
|
+
}
|
|
20350
|
+
/**
|
|
20351
|
+
* Warning for an `Export.Name` that resolved to something containing secret
|
|
20352
|
+
* plaintext. Refused rather than published: the name would be a state KEY, and
|
|
20353
|
+
* keys are never redacted.
|
|
20354
|
+
*
|
|
20355
|
+
* The name is shown MASKED, and omitted entirely if masking somehow left it
|
|
20356
|
+
* unchanged. stderr is a reader like any other, so the invariant is absolute: a
|
|
20357
|
+
* message must never claim a masking it did not perform.
|
|
20358
|
+
*/
|
|
20359
|
+
function secretBearingExportNameWarning(outputKey, exportName, exposure) {
|
|
20360
|
+
const masked = stripControlChars(maskEveryOccurrence(exportName, exposure));
|
|
20361
|
+
const shown = masked === stripControlChars(exportName) ? "" : `(masked: "${masked}") `;
|
|
20362
|
+
return `Output ${stripControlChars(outputKey)} has an Export.Name that resolves to a value containing a secret ${shown}— skipping the export alias. An export name becomes a key in state.json and in the exports index, and redaction rewrites VALUES only, so publishing it would persist the secret in plaintext. Use a non-secret Export.Name.`;
|
|
20363
|
+
}
|
|
20364
|
+
/**
|
|
20365
|
+
* Warning for a state KEY that already holds secret plaintext — the residue an
|
|
20366
|
+
* EARLIER binary left when it published an export name that resolved to one.
|
|
20367
|
+
*
|
|
20368
|
+
* `cdkd scrub` cannot repair this. Every redaction pass rewrites VALUES; a key
|
|
20369
|
+
* is the export's identity, so renaming it here would silently retire an export
|
|
20370
|
+
* consumers resolve by name, and dropping it would delete a live export. The
|
|
20371
|
+
* remedy is in the template: give the output a non-secret `Export.Name` and
|
|
20372
|
+
* redeploy, which rewrites `state.outputs` wholesale and republishes the index.
|
|
20373
|
+
* Reported so the `--dry-run --fail` CI gate stops calling such a state clean.
|
|
20374
|
+
*/
|
|
20375
|
+
function secretBearingStateKeyWarning(stackName, key, exposure) {
|
|
20376
|
+
return `State for ${stripControlChars(stackName)} holds an output KEY containing a secret (masked: "${stripControlChars(maskEveryOccurrence(key, exposure))}") — cdkd scrub cannot rewrite a key, only a value, because the key IS the export name consumers resolve by. Give that output a non-secret Export.Name and redeploy: the next deploy replaces state.outputs and the exports index entirely. ROTATE the exposed secret.`;
|
|
20377
|
+
}
|
|
20378
|
+
/**
|
|
20379
|
+
* Warning for an `Export.Name` colliding with an output NAME it does not own.
|
|
20380
|
+
*
|
|
20381
|
+
* Names both outputs because the two are equally likely to be the mistake, and
|
|
20382
|
+
* says which value survives — the export is skipped, so the key keeps the
|
|
20383
|
+
* output's own value.
|
|
20384
|
+
*
|
|
20385
|
+
* No masking here, and none is needed: the name printed is one that MATCHED a
|
|
20386
|
+
* declared output name, i.e. template text, and a name carrying a secret is
|
|
20387
|
+
* refused by {@link secretBearingExportNameWarning} before this is reached.
|
|
20388
|
+
*/
|
|
20389
|
+
function exportAliasCollisionWarning(outputKey, exportName) {
|
|
20390
|
+
const shown = stripControlChars(exportName);
|
|
20391
|
+
const from = stripControlChars(outputKey);
|
|
20392
|
+
return `Output ${from} exports as "${shown}", which is also the name of another output in this stack — skipping the export alias, so output ${shown} keeps its own value and the export is not published. A consumer's Fn::ImportValue on "${shown}" therefore resolves to output ${shown}, NOT to ${from} (CloudFormation would publish both). Rename the export, or the colliding output.`;
|
|
20393
|
+
}
|
|
20394
|
+
/**
|
|
20395
|
+
* Warning for the same collision seen by `cdkd scrub`, whose remedy differs.
|
|
20396
|
+
*
|
|
20397
|
+
* Scrub does not resolve outputs — it redacts state written by an EARLIER
|
|
20398
|
+
* binary, where the alias may have won the colliding key — so it cannot claim
|
|
20399
|
+
* the key belongs to either output. It drops the position source for that key
|
|
20400
|
+
* and lets the value scan decide from the plaintext actually stored, which is
|
|
20401
|
+
* why this message promises something weaker than the deploy-time one. It can
|
|
20402
|
+
* also fire on a template the deploy handled cleanly, per
|
|
20403
|
+
* {@link collectDeclaredOutputNames}.
|
|
20404
|
+
*/
|
|
20405
|
+
function exportAliasCollisionScrubWarning(outputKey, exportName, secrets) {
|
|
20406
|
+
const exposure = secretsPresentIn(exportName, secrets);
|
|
20407
|
+
const shown = stripControlChars(exposure ? maskEveryOccurrence(exportName, exposure) : exportName);
|
|
20408
|
+
return `Output ${stripControlChars(outputKey)} exports as "${shown}", which is also the name of another output in this stack — state cannot say which of the two the stored value under "${shown}" came from, so that key is redacted by value match instead of by template position, and two references resolving to the same value could still collapse there. Rename the export, or the colliding output, and redeploy.`;
|
|
20409
|
+
}
|
|
20410
|
+
|
|
19953
20411
|
//#endregion
|
|
19954
20412
|
//#region src/utils/colors.ts
|
|
19955
20413
|
/**
|
|
@@ -21526,16 +21984,22 @@ async function resolveReplayProps(props, resolver, secrets) {
|
|
|
21526
21984
|
* deploy reports a change that never converges — the same defect the four
|
|
21527
21985
|
* deploy-side writers had, arriving here through the replay instead.
|
|
21528
21986
|
*
|
|
21529
|
-
* It takes `STATE_DERIVED_RULES`, which is
|
|
21987
|
+
* It takes `STATE_DERIVED_RULES`, which is every relaxation. The source is a
|
|
21530
21988
|
* persisted record, so it holds no PUBLIC expressions (a `String` ssm reference
|
|
21531
21989
|
* is stored resolved) and any `{{resolve:...}}` in it is by construction a
|
|
21532
21990
|
* secret — that is `trustAnyExpression`. And the bag WAS produced by resolving
|
|
21533
21991
|
* that source (`resolveReplayProps` -> the provider's `effectiveProperties`),
|
|
21534
21992
|
* so the two have identical structure and positional array descent is sound —
|
|
21535
|
-
* that is `descendArrays`.
|
|
21536
|
-
*
|
|
21537
|
-
*
|
|
21538
|
-
*
|
|
21993
|
+
* that is `descendArrays`. It is also the SAME generation, resolved one
|
|
21994
|
+
* statement earlier in this call — that is `sourceIsSameGeneration`, and this
|
|
21995
|
+
* writer is one of only two that can honestly claim it.
|
|
21996
|
+
*
|
|
21997
|
+
* Using `STATE_SOURCED_READBACK_RULES` here reads plausible and is wrong in the
|
|
21998
|
+
* quiet direction: it turns positional array descent off. Since issue #1915 a
|
|
21999
|
+
* `Tags[]` / ECS `Environment[]` element is reached by KEYED descent either
|
|
22000
|
+
* way, so the concrete loss is narrower than it was — it is a list whose
|
|
22001
|
+
* elements carry no `Name` / `Key` identity, which only positional descent can
|
|
22002
|
+
* walk.
|
|
21539
22003
|
*
|
|
21540
22004
|
* No-op when the op resolved no secret.
|
|
21541
22005
|
*/
|
|
@@ -22107,7 +22571,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
22107
22571
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
22108
22572
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
22109
22573
|
function getCdkdVersion() {
|
|
22110
|
-
return "0.283.
|
|
22574
|
+
return "0.283.32";
|
|
22111
22575
|
}
|
|
22112
22576
|
/**
|
|
22113
22577
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -22748,6 +23212,14 @@ var DeployEngine = class {
|
|
|
22748
23212
|
*/
|
|
22749
23213
|
outputsTemplateSource = {};
|
|
22750
23214
|
/**
|
|
23215
|
+
* Whether {@link outputsTemplateSource} may be used to POSITION the outputs
|
|
23216
|
+
* redaction. False once an outputs pass threw partway: the post-loop
|
|
23217
|
+
* name pass never ran, so the bag holds only the alias keys written before
|
|
23218
|
+
* the throw — a partial source built from THIS template, while the bag the
|
|
23219
|
+
* failure path then redacts is the PREVIOUS deploy's. Reset per `deploy()`.
|
|
23220
|
+
*/
|
|
23221
|
+
outputsSourceUsable = true;
|
|
23222
|
+
/**
|
|
22751
23223
|
* Per-logical-id snapshot of the intrinsic-RESOLVED desired properties
|
|
22752
23224
|
* each CREATE / UPDATE attempted (issue #1198). Written just before the
|
|
22753
23225
|
* provider call; read only when the op FAILS, to journal the failed op's
|
|
@@ -22792,6 +23264,7 @@ var DeployEngine = class {
|
|
|
22792
23264
|
this.perResourceTemplateProps = /* @__PURE__ */ new Map();
|
|
22793
23265
|
this.outputSecrets = /* @__PURE__ */ new Map();
|
|
22794
23266
|
this.outputsTemplateSource = {};
|
|
23267
|
+
this.outputsSourceUsable = true;
|
|
22795
23268
|
this.resolver.resetPhysicalIdFallbackCount();
|
|
22796
23269
|
return withStackName(stackName, () => this.doDeploy(stackName, template));
|
|
22797
23270
|
}
|
|
@@ -22835,7 +23308,7 @@ var DeployEngine = class {
|
|
|
22835
23308
|
*/
|
|
22836
23309
|
redactOutputs(outputs) {
|
|
22837
23310
|
if (this.outputSecrets.size === 0) return outputs;
|
|
22838
|
-
return redactSecretsForState(outputs, this.outputSecrets, this.outputsTemplateSource);
|
|
23311
|
+
return redactSecretsForState(outputs, this.outputSecrets, this.outputsSourceUsable ? this.outputsTemplateSource : void 0);
|
|
22839
23312
|
}
|
|
22840
23313
|
/**
|
|
22841
23314
|
* Redact resolved secret plaintext out of rollback-journal operations (GHSA
|
|
@@ -24439,9 +24912,30 @@ var DeployEngine = class {
|
|
|
24439
24912
|
});
|
|
24440
24913
|
}
|
|
24441
24914
|
/**
|
|
24442
|
-
*
|
|
24915
|
+
* What a failed Output resolution does, shared by both passes of
|
|
24916
|
+
* {@link resolveOutputs} so they cannot drift — the alias pass reports the
|
|
24917
|
+
* SAME failure for a name it could not resolve as the value pass does for a
|
|
24918
|
+
* value, which is what the single-pass shape did when both lived in one
|
|
24919
|
+
* `try`.
|
|
24920
|
+
*
|
|
24921
|
+
* Issue #1111 item 2: under `--strict-getatt` an unresolvable Output fails
|
|
24922
|
+
* the deploy instead of silently publishing nothing (which breaks downstream
|
|
24923
|
+
* `Fn::ImportValue` consumers with "export not found" long after this deploy
|
|
24924
|
+
* exits 0).
|
|
24925
|
+
*/
|
|
24926
|
+
handleOutputResolutionFailure(error, outputKey, outputs) {
|
|
24927
|
+
if (this.options.strictGetAtt) throw new Error(`Failed to resolve output ${outputKey}: ${error instanceof Error ? error.message : String(error)} (--strict-getatt promotes output resolution failures to deploy errors; drop the flag to warn and skip the output instead)`, { cause: error });
|
|
24928
|
+
this.logger.warn(`Failed to resolve output ${outputKey}: ${String(error)}`);
|
|
24929
|
+
outputs[outputKey] = void 0;
|
|
24930
|
+
}
|
|
24931
|
+
/**
|
|
24932
|
+
* Resolve stack outputs from template and resource attributes.
|
|
24443
24933
|
*
|
|
24444
|
-
* Uses IntrinsicFunctionResolver for full CloudFormation intrinsic function
|
|
24934
|
+
* Uses `IntrinsicFunctionResolver` for full CloudFormation intrinsic function
|
|
24935
|
+
* support, and runs in TWO passes — every value, then every export alias —
|
|
24936
|
+
* so an alias decision sees the complete set of secrets this pass resolved
|
|
24937
|
+
* rather than whatever the declaration order happened to have recorded by
|
|
24938
|
+
* then (issue #1919).
|
|
24445
24939
|
*/
|
|
24446
24940
|
async resolveOutputs(template, resources, stackName, parameterValues, conditions) {
|
|
24447
24941
|
if (!template.Outputs) return {};
|
|
@@ -24452,29 +24946,58 @@ var DeployEngine = class {
|
|
|
24452
24946
|
...parameterValues && { parameters: parameterValues },
|
|
24453
24947
|
...conditions && { conditions }
|
|
24454
24948
|
}, stackName);
|
|
24455
|
-
|
|
24456
|
-
|
|
24457
|
-
|
|
24458
|
-
|
|
24949
|
+
const publishedOutputNames = collectPublishedOutputNames(template.Outputs, conditions);
|
|
24950
|
+
let outputsPassCompleted = false;
|
|
24951
|
+
try {
|
|
24952
|
+
for (const [outputKey, output] of Object.entries(template.Outputs)) {
|
|
24953
|
+
if (isOutputSuppressedByCondition(output, conditions)) {
|
|
24954
|
+
this.logger.debug(`Skipping output ${outputKey} — condition ${output.Condition} is false`);
|
|
24955
|
+
continue;
|
|
24956
|
+
}
|
|
24957
|
+
try {
|
|
24958
|
+
outputs[outputKey] = await this.resolver.resolve(output.Value, context);
|
|
24959
|
+
} catch (error) {
|
|
24960
|
+
this.handleOutputResolutionFailure(error, outputKey, outputs);
|
|
24961
|
+
}
|
|
24459
24962
|
}
|
|
24460
|
-
|
|
24461
|
-
|
|
24462
|
-
|
|
24463
|
-
|
|
24464
|
-
|
|
24465
|
-
|
|
24466
|
-
|
|
24467
|
-
|
|
24963
|
+
for (const [outputKey, output] of Object.entries(template.Outputs)) {
|
|
24964
|
+
if (isOutputSuppressedByCondition(output, conditions)) continue;
|
|
24965
|
+
if (!output.Export?.Name) continue;
|
|
24966
|
+
const value = outputs[outputKey];
|
|
24967
|
+
if (value === void 0) continue;
|
|
24968
|
+
const nameSecrets = /* @__PURE__ */ new Map();
|
|
24969
|
+
let exportName;
|
|
24970
|
+
try {
|
|
24971
|
+
try {
|
|
24972
|
+
exportName = typeof output.Export.Name === "string" ? output.Export.Name : await this.resolver.resolve(output.Export.Name, {
|
|
24973
|
+
...context,
|
|
24974
|
+
recordedSecretValues: nameSecrets
|
|
24975
|
+
});
|
|
24976
|
+
} finally {
|
|
24977
|
+
for (const [plaintext, expression] of nameSecrets) context.recordedSecretValues?.set(plaintext, expression);
|
|
24468
24978
|
}
|
|
24979
|
+
} catch (error) {
|
|
24980
|
+
this.handleOutputResolutionFailure(error, outputKey, outputs);
|
|
24981
|
+
continue;
|
|
24982
|
+
}
|
|
24983
|
+
if (typeof exportName !== "string") continue;
|
|
24984
|
+
const exposure = exportNameSecretExposure(exportName, nameSecrets, context.recordedSecretValues);
|
|
24985
|
+
if (exposure) this.logger.warn(secretBearingExportNameWarning(outputKey, exportName, exposure));
|
|
24986
|
+
else if (isExportAliasCollision(exportName, outputKey, publishedOutputNames)) this.logger.warn(exportAliasCollisionWarning(outputKey, exportName));
|
|
24987
|
+
else {
|
|
24988
|
+
outputs[exportName] = value;
|
|
24989
|
+
this.outputsTemplateSource[exportName] = output.Value;
|
|
24469
24990
|
}
|
|
24470
|
-
} catch (error) {
|
|
24471
|
-
if (this.options.strictGetAtt) throw new Error(`Failed to resolve output ${outputKey}: ${error instanceof Error ? error.message : String(error)} (--strict-getatt promotes output resolution failures to deploy errors; drop the flag to warn and skip the output instead)`, { cause: error });
|
|
24472
|
-
this.logger.warn(`Failed to resolve output ${outputKey}: ${String(error)}`);
|
|
24473
|
-
outputs[outputKey] = void 0;
|
|
24474
24991
|
}
|
|
24992
|
+
outputsPassCompleted = true;
|
|
24993
|
+
} finally {
|
|
24994
|
+
if (context.recordedSecretValues) for (const [value, expr] of context.recordedSecretValues) this.outputSecrets.set(value, expr);
|
|
24995
|
+
if (!outputsPassCompleted) this.outputsSourceUsable = false;
|
|
24996
|
+
}
|
|
24997
|
+
for (const [outputKey, output] of Object.entries(template.Outputs)) {
|
|
24998
|
+
if (!publishedOutputNames.has(outputKey)) continue;
|
|
24999
|
+
this.outputsTemplateSource[outputKey] = output.Value;
|
|
24475
25000
|
}
|
|
24476
|
-
if (context.recordedSecretValues) for (const [value, expr] of context.recordedSecretValues) this.outputSecrets.set(value, expr);
|
|
24477
|
-
for (const [outputKey, output] of Object.entries(template.Outputs)) this.outputsTemplateSource[outputKey] = output.Value;
|
|
24478
25001
|
return outputs;
|
|
24479
25002
|
}
|
|
24480
25003
|
buildDisplayOutputs(template, resolvedOutputs) {
|
|
@@ -24489,5 +25012,5 @@ var DeployEngine = class {
|
|
|
24489
25012
|
};
|
|
24490
25013
|
|
|
24491
25014
|
//#endregion
|
|
24492
|
-
export {
|
|
24493
|
-
//# sourceMappingURL=deploy-engine-
|
|
25015
|
+
export { getAccountInfo as $, isCdkdError as $n, runDockerForeground as $t, cyan as A, getAwsClients as An, S3StateBackend as At, stateKeySecretExposure as B, LocalStartServiceError as Bn, escapeRegExp$1 as Bt, makeCanonicalizePropertiesFn as C, canonicalizeRegion as Cn, DiffCalculator as Ct, renderStatefulReason as D, clearBucketRegionCache as Dn, DagBuilder as Dt, isStatefulRecreateTargetSync as E, processStackMessages as En, withRetry as Et, collectDeclaredOutputNames as F, ConfigError as Fn, WorkGraph as Ft, findActionableSilentDrops as G, ProvisioningError as Gn, getBootstrapMarkerKey as Gt, collectInlinePolicyNamesManagedBySiblings as H, MissingCdkCliError as Hn, AssetModeResolver as Ht, collectPublishedOutputNames as I, DependencyError as In, buildAssetRedirectMap as It, slowCcOperationTimeoutMs as J, StackHasActiveImportsError as Jn, validateContainerRepoName as Jt, findSilentDropProperties as K, ResourceTimeoutError as Kn, parseBootstrapMarker as Kt, exportAliasCollisionScrubWarning as L, DeployCancelledError as Ln, createAssetRedirectResolver as Lt, green as M, setAwsClients as Mn, shouldRetainResource as Mt, red as N, AssetError as Nn, AssetPublisher as Nt, formatResourceLine as O, resolveBucketRegion as On, TemplateParser as Ot, yellow as P, CdkdError as Pn, stringifyValue as Pt, cfnRefValueFromPhysicalId as Q, formatError as Qn, getDockerCmd as Qt, isExportAliasCollision as R, LocalInvokeBuildError as Rn, loadPublishableAssetManifest as Rt, unsupportedFinalSnapshotError as S, PARTITION_TABLE as Sn, applyRoleArnIfSet as St, MULTI_REGION_RECREATE_BLOCKED_TYPES as T, AssemblyReader as Tn, describeTypeWithThrottleRetry as Tt, clearOnUpdateRemoval as U, NestedStackChildDirectDestroyError as Un, BOOTSTRAP_MARKER_PREFIX as Ut, IAMRoleProvider as V, LockError as Vn, stripControlChars as Vt, ProviderRegistry as W, PartialFailureError as Wn, ensureAssetStorage as Wt, isTerminationProtectionPropagationError as X, StateError as Xn, buildDockerImage as Xt, disableInstanceApiTermination as Y, StackTerminationProtectionError as Yn, buildDenyExternalAccessPolicy as Yt, IntrinsicFunctionResolver as Z, SynthesisError as Zn, formatDockerLoginError as Zt, buildFinalSnapshotIdentifier as _, CFN_TEMPLATE_URL_LIMIT as _n, s3BucketArn as _t, DeploymentEventsStore as a, getDefaultStateBucketName as an, markNonRetryable as ar, coerceCfnBoolean as at, isFinalSnapshotError as b, uploadCfnTemplate as bn, s3BucketRegionalDomainName as bt, replayFailedOperations as c, resolveAutoAssetStorage as cn, readConfigString as ct, deleteSkipReason as d, resolveStateBucketWithDefault as dn, requireConfigObject as dt, runDockerStreaming as en, normalizeAwsError as er, refStateLookupFromResource as et, withResourceDeadline as f, resolveStateBucketWithDefaultAndSource as fn, requireConfigString as ft, PRE_DELETE_SNAPSHOT_TYPES as g, CFN_TEMPLATE_BODY_LIMIT as gn, scrubResourceRecord as gt, ATOMIC_FINAL_SNAPSHOT_TYPES as h, warnDeprecatedNoPrefixCliFlag as hn, redactSecretsForState as ht, DeploymentEventsReader as i, synthesisStatusMessage as in, isThrottlingError as ir, assertRegionMatch as it, gray as j, resetAwsClients as jn, rebuildClientForBucketRegion as jt, bold as k, AwsClients as kn, LockManager as kt, replayRollback as l, resolveCaptureObservedState as ln, replayWarn as lt, computeImplicitDeleteEdges as m, stateBucketExistenceConfirmed as mn, TEMPLATE_SOURCED_RULES as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, getDockerImageBySourceHash as nn, isMarkedNonRetryable as nr, normalizeAwsTagsToCfn as nt, planFailedOps as o, getLegacyStateBucketName as on, __exportAll as or, configBooleanRefusal as ot, IMPLICIT_DELETE_DEPENDENCIES as p, resolveUseCdkBootstrapAssets as pn, STATE_SOURCED_CROSS_GENERATION_RULES as pt, CloudControlProvider as q, ResourceUpdateNotSupportedError as qn, validateAssetBucketName as qt, DeployEngine as r, Synthesizer as rn, isRetryableTransientError as rr, resolveExplicitPhysicalId as rt, planRollback as s, resolveApp as sn, configStringRefusal as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, AssetManifestLoader as tn, withErrorHandling as tr, WAFv2WebACLProvider as tt, UNSPECIFIED_SKIP_REASON as u, resolveSkipPrefix as un, requireConfigArray as ut, ccRoutedFinalSnapshotError as v, MIGRATE_TMP_PREFIX as vn, s3BucketDomainName as vt, extractDeploymentEventError as w, derivePartitionAndUrlSuffix as wn, INTRINSIC_KEYS as wt, refusesFinalSnapshot as x, expectedOwnerParam as xn, s3BucketWebsiteUrl as xt, createPreDeleteFinalSnapshot as y, findLargeInlineResources as yn, s3BucketDualStackDomainName as yt, secretBearingStateKeyWarning as z, LocalMigrateError as zn, rewriteTemplateAssetReferences as zt };
|
|
25016
|
+
//# sourceMappingURL=deploy-engine-rYTNzst0.js.map
|