@go-to-k/cdkd 0.283.23 → 0.283.25
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-BKIOy3_q.js → asg-provider-BqKgMaXN.js} +2 -2
- package/dist/{asg-provider-BKIOy3_q.js.map → asg-provider-BqKgMaXN.js.map} +1 -1
- package/dist/cli.js +20 -7
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-CQXpKq7o.js → deploy-engine-MHqWGHl9.js} +616 -65
- package/dist/deploy-engine-MHqWGHl9.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-CQXpKq7o.js.map +0 -1
|
@@ -9821,6 +9821,81 @@ function s3BucketWebsiteUrl(bucketName, region) {
|
|
|
9821
9821
|
/** Fixed marker substituted for a secret value in log / error output. */
|
|
9822
9822
|
const SECRET_MASK = "***";
|
|
9823
9823
|
/**
|
|
9824
|
+
* Every `{{resolve:...}}` expression this process has PROVEN resolves to a
|
|
9825
|
+
* secret, as a SET — uncollapsed by resolved value (issue #1910).
|
|
9826
|
+
*
|
|
9827
|
+
* This is the piece {@link RecordedSecretValues} structurally cannot supply.
|
|
9828
|
+
* That map is keyed by the resolved PLAINTEXT, so when two expressions resolve
|
|
9829
|
+
* to the same value it keeps only the last, and asking it whether the LOSING
|
|
9830
|
+
* expression was a secret answers "no" — for precisely the pair the path-based
|
|
9831
|
+
* redaction exists to separate. The losing leaf then falls through to the value
|
|
9832
|
+
* scan and is persisted holding its SIBLING's expression: a permanent spurious
|
|
9833
|
+
* UPDATE, and on the rollback-journal replay path a re-resolution of the wrong
|
|
9834
|
+
* reference against the live resource.
|
|
9835
|
+
*
|
|
9836
|
+
* EVERY secret expression is recorded, not only the `ssm` ones (issue
|
|
9837
|
+
* [#1916](https://github.com/go-to-k/cdkd/issues/1916)). Only `ssm` needs an
|
|
9838
|
+
* entry to answer "is this secret?" — a `secretsmanager` reference is secret by
|
|
9839
|
+
* SPELLING (see {@link isKnownSecretExpression}), decidable with no lookup and
|
|
9840
|
+
* no memory, while an `ssm` one is secret only when its parameter is a
|
|
9841
|
+
* `SecureString`, knowable only from the `GetParameter` response. But this set
|
|
9842
|
+
* answers a SECOND question: it is the CANDIDATE LIST
|
|
9843
|
+
* {@link positionByIntrinsicSkeleton} matches an intrinsic source leaf against,
|
|
9844
|
+
* and there the losing member of a collapsed
|
|
9845
|
+
* secretsmanager/secretsmanager pair must be nameable too. Holding only the ssm
|
|
9846
|
+
* half made the set's name a lie and left that pair unpositionable.
|
|
9847
|
+
*
|
|
9848
|
+
* One kind is deliberately still absent: an `ssm` reference whose `Type` came
|
|
9849
|
+
* back unclassifiable is treated as secret for THAT resolution but not pinned,
|
|
9850
|
+
* so the next pass re-asks AWS rather than inheriting a transient answer
|
|
9851
|
+
* (issue #1901). Recording it here would pin it for the process.
|
|
9852
|
+
*
|
|
9853
|
+
* It lives in THIS module rather than in the resolver even though the resolver
|
|
9854
|
+
* is its only writer, for two reasons. This module is the LEAF — the resolver
|
|
9855
|
+
* already imports it, so the store is reachable from the writer without adding
|
|
9856
|
+
* an edge, while the reverse (a leaf importing the resolver) would close a
|
|
9857
|
+
* cycle. And the READER is {@link isKnownSecretExpression} right here, so
|
|
9858
|
+
* homing it here means no call site has to thread it: the four sibling writers
|
|
9859
|
+
* #1910 fixes pass a position SOURCE and nothing else.
|
|
9860
|
+
*
|
|
9861
|
+
* Its lifetime matches the resolver's `cachedDynamicReferences` — the resolver
|
|
9862
|
+
* clears both together, and that shared lifetime is the point: this set IS the
|
|
9863
|
+
* resolver's own SecureString verdict store, which already had exactly this
|
|
9864
|
+
* scope, so nothing is widened by homing it here.
|
|
9865
|
+
*
|
|
9866
|
+
* Process-wide is therefore INHERITED, not claimed to be ideal. It is not
|
|
9867
|
+
* strictly sound across regions or accounts in one run — the same expression
|
|
9868
|
+
* can name a `SecureString` in one region and a plain `String` in another — but
|
|
9869
|
+
* `cachedDynamicReferences` caches the resolved VALUE under the same
|
|
9870
|
+
* assumption, so a per-region store here would fix nothing on its own. Note
|
|
9871
|
+
* which way the imprecision points: an entry only ever GRANTS "persist the
|
|
9872
|
+
* source leaf verbatim", and the leaf is the resource's own template
|
|
9873
|
+
* expression, so the failure mode is a public reference stored as an expression
|
|
9874
|
+
* (a spurious UPDATE, issue #1901's class), never a secret stored as plaintext.
|
|
9875
|
+
*/
|
|
9876
|
+
const recordedSecretExpressions$1 = /* @__PURE__ */ new Set();
|
|
9877
|
+
/** Remember that `expression` resolves to a secret. Called by the resolver. */
|
|
9878
|
+
function recordSecretExpression(expression) {
|
|
9879
|
+
recordedSecretExpressions$1.add(expression);
|
|
9880
|
+
}
|
|
9881
|
+
/**
|
|
9882
|
+
* Forget a previously recorded expression — the resolver's `SecureString`
|
|
9883
|
+
* verdict going the other way (an ssm parameter that turns out to be a plain
|
|
9884
|
+
* `String` / `StringList`, i.e. public config that must stay RESOLVED in
|
|
9885
|
+
* state).
|
|
9886
|
+
*/
|
|
9887
|
+
function forgetSecretExpression(expression) {
|
|
9888
|
+
recordedSecretExpressions$1.delete(expression);
|
|
9889
|
+
}
|
|
9890
|
+
/** Has `expression` been PROVEN to resolve to a secret this process? */
|
|
9891
|
+
function isRecordedSecretExpression(expression) {
|
|
9892
|
+
return recordedSecretExpressions$1.has(expression);
|
|
9893
|
+
}
|
|
9894
|
+
/** Drop every remembered verdict. Paired with the resolver's cache reset. */
|
|
9895
|
+
function clearRecordedSecretExpressions() {
|
|
9896
|
+
recordedSecretExpressions$1.clear();
|
|
9897
|
+
}
|
|
9898
|
+
/**
|
|
9824
9899
|
* A resolved secret value shorter than this is NOT used as a redaction needle:
|
|
9825
9900
|
* a 1-2 character plaintext (e.g. a secret whose JSON key holds `"0"`) would
|
|
9826
9901
|
* match incidental characters everywhere and mangle unrelated state. Such a
|
|
@@ -9842,6 +9917,382 @@ function buildNeedleRegex(values) {
|
|
|
9842
9917
|
if (needles.length === 0) return void 0;
|
|
9843
9918
|
return new RegExp(needles.map(escapeRegExp).join("|"), "g");
|
|
9844
9919
|
}
|
|
9920
|
+
/** The bag was produced by resolving the source: same shape, template source. */
|
|
9921
|
+
const TEMPLATE_DERIVED_RULES = {
|
|
9922
|
+
descendArrays: true,
|
|
9923
|
+
trustAnyExpression: false
|
|
9924
|
+
};
|
|
9925
|
+
/** An AWS readback projected from a persisted STATE bag. */
|
|
9926
|
+
const STATE_SOURCED_READBACK_RULES = {
|
|
9927
|
+
descendArrays: false,
|
|
9928
|
+
trustAnyExpression: true
|
|
9929
|
+
};
|
|
9930
|
+
/** An AWS readback projected from the TEMPLATE: neither relaxation applies. */
|
|
9931
|
+
const TEMPLATE_SOURCED_READBACK_RULES = {
|
|
9932
|
+
descendArrays: false,
|
|
9933
|
+
trustAnyExpression: false
|
|
9934
|
+
};
|
|
9935
|
+
/**
|
|
9936
|
+
* The bag was produced by resolving a STATE source — both relaxations apply.
|
|
9937
|
+
*
|
|
9938
|
+
* The rollback replay is the case (issue #1910): `resolveReplayProps` resolves
|
|
9939
|
+
* the JOURNALED bag and the provider's `effectiveProperties` come back from
|
|
9940
|
+
* that, so the two have identical structure and positional array descent is
|
|
9941
|
+
* sound exactly as it is for a template-derived bag. And the source is a
|
|
9942
|
+
* persisted record, which holds no PUBLIC expressions — a `String` ssm
|
|
9943
|
+
* reference is stored resolved — so any `{{resolve:...}}` in it is by
|
|
9944
|
+
* construction a secret.
|
|
9945
|
+
*
|
|
9946
|
+
* Using `STATE_SOURCED_READBACK_RULES` here instead would be wrong in the
|
|
9947
|
+
* quiet direction: it turns array descent OFF, so a secret nested in a
|
|
9948
|
+
* `Tags[]` / ECS `secrets[]` element falls to the value scan and a colliding
|
|
9949
|
+
* pair still collapses there.
|
|
9950
|
+
*/
|
|
9951
|
+
const STATE_DERIVED_RULES = {
|
|
9952
|
+
descendArrays: true,
|
|
9953
|
+
trustAnyExpression: true
|
|
9954
|
+
};
|
|
9955
|
+
function isDynamicReferenceString(value) {
|
|
9956
|
+
return typeof value === "string" && value.includes("{{resolve:");
|
|
9957
|
+
}
|
|
9958
|
+
/**
|
|
9959
|
+
* Is this template expression one whose resolved value is a SECRET?
|
|
9960
|
+
*
|
|
9961
|
+
* Two independent answers, and both are needed:
|
|
9962
|
+
*
|
|
9963
|
+
* - A `secretsmanager` reference is secret BY DEFINITION, so spelling settles
|
|
9964
|
+
* it with no lookup. This arm is what makes the #1904 fix work at all: when
|
|
9965
|
+
* two expressions resolve to the same value the value-keyed map keeps only the
|
|
9966
|
+
* last, so asking the map whether the LOSING expression was a secret answers
|
|
9967
|
+
* "no" — precisely for the pair the fix exists to separate.
|
|
9968
|
+
* - An `ssm` reference is secret only when its parameter is a `SecureString`
|
|
9969
|
+
* (issue #1901), which is not derivable from the string, so that arm consults
|
|
9970
|
+
* what the resolver actually recorded.
|
|
9971
|
+
*
|
|
9972
|
+
* `secretExpressions` is what closes the ssm/ssm case (issue #1910). Derived
|
|
9973
|
+
* from the value-keyed map it is useless for exactly this question — the map
|
|
9974
|
+
* already collapsed the pair, so the losing expression is absent from
|
|
9975
|
+
* `secrets.values()` — which is why callers pass the resolver's own SET of
|
|
9976
|
+
* secret expressions instead. Callers that pass nothing fall back to the map's
|
|
9977
|
+
* values, i.e. to the pre-#1910 behavior: the pair still collapses, but nothing
|
|
9978
|
+
* leaks (both leaves are redacted, just onto one expression).
|
|
9979
|
+
*/
|
|
9980
|
+
function isKnownSecretExpression(expression, secretExpressions) {
|
|
9981
|
+
return expression.startsWith("{{resolve:secretsmanager:") || secretExpressions.has(expression) || isRecordedSecretExpression(expression);
|
|
9982
|
+
}
|
|
9983
|
+
/**
|
|
9984
|
+
* Is this source leaf a SINGLE complete `{{resolve:...}}` token and nothing
|
|
9985
|
+
* else?
|
|
9986
|
+
*
|
|
9987
|
+
* Whole-leaf substitution is only correct for that shape. A MIXED leaf --
|
|
9988
|
+
* `pre{{resolve:ssm:/public}}-{{resolve:secretsmanager:x}}post`, i.e. anything
|
|
9989
|
+
* the resolver substituted INTO rather than replaced -- must fall to the value
|
|
9990
|
+
* scan, which rewrites just the secret substring. Substituting the whole leaf
|
|
9991
|
+
* there would re-introduce every other token in it, including a public ssm
|
|
9992
|
+
* reference the resolver deliberately left resolved (issue #1901).
|
|
9993
|
+
*/
|
|
9994
|
+
function isSingleDynamicReferenceToken(value) {
|
|
9995
|
+
return /^\{\{resolve:[^{}]*\}\}$/.test(value);
|
|
9996
|
+
}
|
|
9997
|
+
function isPlainObject$2(value) {
|
|
9998
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9999
|
+
}
|
|
10000
|
+
/**
|
|
10001
|
+
* Stands in for a source part the skeleton cannot know — an `Fn::Join` element
|
|
10002
|
+
* that is itself an intrinsic, or an `Fn::Sub` `${...}` variable.
|
|
10003
|
+
*
|
|
10004
|
+
* `[^}]*` rather than `.*` because a recorded expression's INNER text never
|
|
10005
|
+
* contains `}`: the resolver matches them with `/\{\{resolve:([^}]+)\}\}/`, so
|
|
10006
|
+
* the first `}` after `{{resolve:` is already the terminator. Excluding it
|
|
10007
|
+
* means a wildcard can never swallow one token's terminator and run into the
|
|
10008
|
+
* next, so a skeleton for ONE reference cannot match a candidate built from a
|
|
10009
|
+
* different one.
|
|
10010
|
+
*/
|
|
10011
|
+
const SKELETON_WILDCARD = "[^}]*";
|
|
10012
|
+
/**
|
|
10013
|
+
* More wildcards than this and the skeleton is REFUSED outright.
|
|
10014
|
+
*
|
|
10015
|
+
* The bound is what actually closes the backtracking class; the collapse below
|
|
10016
|
+
* only closes one ARRANGEMENT of it. A skeleton with N wildcards makes a
|
|
10017
|
+
* FAILING match exponential in N — and failing is the common case, since the
|
|
10018
|
+
* pattern is tried against every recorded expression that is NOT this leaf's.
|
|
10019
|
+
* The collapse merges ADJACENT wildcards, so `${a}${b}${c}` becomes one; but
|
|
10020
|
+
* wildcards separated by a literal cannot merge and backtrack identically.
|
|
10021
|
+
* Measured against a failing candidate: `Fn::Sub '${a}x${b}x…'` at 6 wildcards
|
|
10022
|
+
* 17.7s and at 8 did not finish in two minutes, and a realistic
|
|
10023
|
+
* `Fn::Join['-', 9 x {Ref}]` against a hyphen-rich secret ARN took 855ms per
|
|
10024
|
+
* candidate. So the producer is the wildcard COUNT, not adjacency.
|
|
10025
|
+
*
|
|
10026
|
+
* What refusing costs is worth stating plainly rather than waving away, because
|
|
10027
|
+
* it is the SAME cost this module's residual note describes: a refused leaf
|
|
10028
|
+
* falls to the value scan, and for a colliding pair that means state holds the
|
|
10029
|
+
* SIBLING's reference, which `resolveReplayProps` re-resolves and applies. A
|
|
10030
|
+
* four-wildcard join carrying substantive literals CAN position uniquely, so
|
|
10031
|
+
* the bound does give something up. It is set where it is because the dominant
|
|
10032
|
+
* CDK shape carries exactly ONE wildcard (the secret ARN's `{Ref}`) and two or
|
|
10033
|
+
* three covers an account plus a region — and because the alternative at the
|
|
10034
|
+
* top of that trade is not "slightly better redaction" but a deploy that hangs
|
|
10035
|
+
* after its AWS mutations.
|
|
10036
|
+
*/
|
|
10037
|
+
const MAX_SKELETON_WILDCARDS = 3;
|
|
10038
|
+
/**
|
|
10039
|
+
* A candidate longer than this REFUSES the whole positioning pass.
|
|
10040
|
+
*
|
|
10041
|
+
* The wildcard cap bounds the EXPONENT; this bounds the BASE. Cost at the cap
|
|
10042
|
+
* is polynomial in the candidate's length (~cubic, measured: 200 chars 4.9ms,
|
|
10043
|
+
* 1000 chars 192ms, and an adversarial all-separator 3000-char candidate 4.1s),
|
|
10044
|
+
* and candidate length is template-authored — `[^}]*` never crosses a `}`, so
|
|
10045
|
+
* the whole expression is one backtracking run. A real `{{resolve:...}}` is
|
|
10046
|
+
* 100-250 characters even with a full ARN and a JSON key, so this only excludes
|
|
10047
|
+
* the pathological.
|
|
10048
|
+
*
|
|
10049
|
+
* The whole pass is refused rather than that one candidate being SKIPPED, and
|
|
10050
|
+
* the difference is load-bearing: skipping shrinks the candidate set, so a
|
|
10051
|
+
* second match could go unseen and condition 2's "exactly one" would be decided
|
|
10052
|
+
* over a filtered list — turning a bound meant for speed into a silent
|
|
10053
|
+
* weakening of the fence. Refusing degrades to the value scan like every other
|
|
10054
|
+
* refusal here.
|
|
10055
|
+
*/
|
|
10056
|
+
const MAX_SKELETON_CANDIDATE_LENGTH = 512;
|
|
10057
|
+
/**
|
|
10058
|
+
* Concatenate skeleton segments, dropping an empty one and COLLAPSING a run of
|
|
10059
|
+
* consecutive wildcards into one — then REFUSE (return `undefined`) when more
|
|
10060
|
+
* than {@link MAX_SKELETON_WILDCARDS} survive.
|
|
10061
|
+
*
|
|
10062
|
+
* The collapse is a correctness fix, not tidiness: `[^}]*[^}]*` is semantically
|
|
10063
|
+
* identical to `[^}]*`, but the engine has to try every split of the input
|
|
10064
|
+
* between them. Measured on a ~120-char candidate, before the collapse:
|
|
10065
|
+
* 4 adjacent wildcards 39ms, 5 ~1s, 6 20s, 8 did not finish in two minutes. Two
|
|
10066
|
+
* legal CFn shapes CDK can emit produce ADJACENT ones — an `Fn::Join` with an
|
|
10067
|
+
* EMPTY delimiter and consecutive non-string parts, and an `Fn::Sub` with
|
|
10068
|
+
* adjacent `${a}${b}` variables — and the collapse takes both to a single
|
|
10069
|
+
* wildcard. It does NOT close the class, which is why the cap exists beside it.
|
|
10070
|
+
*
|
|
10071
|
+
* This runs on the state-persist choke point, i.e. AFTER the AWS mutations and
|
|
10072
|
+
* BEFORE state is written, so a hang there strands real resources.
|
|
10073
|
+
*
|
|
10074
|
+
* An empty segment is dropped rather than appended because an empty DELIMITER
|
|
10075
|
+
* would otherwise sit between two wildcards and defeat the collapse. A
|
|
10076
|
+
* non-empty literal between two wildcards is what anchors the match, and those
|
|
10077
|
+
* are left exactly as they are — and counted.
|
|
10078
|
+
*/
|
|
10079
|
+
function joinSkeletonSegments(segments) {
|
|
10080
|
+
const out = [];
|
|
10081
|
+
let wildcards = 0;
|
|
10082
|
+
for (const segment of segments) {
|
|
10083
|
+
if (segment === "") continue;
|
|
10084
|
+
if (segment === SKELETON_WILDCARD) {
|
|
10085
|
+
if (out[out.length - 1] === SKELETON_WILDCARD) continue;
|
|
10086
|
+
wildcards += 1;
|
|
10087
|
+
if (wildcards > MAX_SKELETON_WILDCARDS) return void 0;
|
|
10088
|
+
}
|
|
10089
|
+
out.push(segment);
|
|
10090
|
+
}
|
|
10091
|
+
return out.join("");
|
|
10092
|
+
}
|
|
10093
|
+
/**
|
|
10094
|
+
* Build an anchored pattern describing what a `{{resolve:...}}` expression at
|
|
10095
|
+
* this INTRINSIC source leaf must look like — literal parts kept verbatim,
|
|
10096
|
+
* unknowable parts wildcarded (issue #1916).
|
|
10097
|
+
*
|
|
10098
|
+
* The dominant CDK shape is an `Fn::Join`, because `secret.secretValueFromJson(...)`
|
|
10099
|
+
* renders the secret's ARN as a `Ref` and CDK joins the pieces:
|
|
10100
|
+
*
|
|
10101
|
+
* ```json
|
|
10102
|
+
* {"Fn::Join": ["", ["{{resolve:secretsmanager:my-secret-", {"Ref": "AWS::AccountId"},
|
|
10103
|
+
* ":SecretString:password}}"]]}
|
|
10104
|
+
* ```
|
|
10105
|
+
*
|
|
10106
|
+
* which yields `^\{\{resolve:secretsmanager:my\-secret\-[^}]*:SecretString:password\}\}$`
|
|
10107
|
+
* — enough to tell that leaf's expression from its `:AWSCURRENT`-suffixed
|
|
10108
|
+
* sibling, which is precisely what the value map cannot do once the two
|
|
10109
|
+
* resolve to the same plaintext. `Fn::Sub` has the same shape via its literal
|
|
10110
|
+
* template string.
|
|
10111
|
+
*
|
|
10112
|
+
* Returns `undefined` for any source this cannot describe (a delimiter that is
|
|
10113
|
+
* itself an intrinsic, a non-array `Fn::Join`, a non-string `Fn::Sub`
|
|
10114
|
+
* template, an object that is not a single-key intrinsic at all), and the
|
|
10115
|
+
* caller then falls back to the value scan — i.e. to the pre-#1916 behavior.
|
|
10116
|
+
*
|
|
10117
|
+
* Exported for its TEST only. The reason is now CONVENIENCE rather than
|
|
10118
|
+
* necessity, and the distinction is worth keeping straight: before the wildcard
|
|
10119
|
+
* cap existed, driving the collapse through `redactSecretsForState` did not
|
|
10120
|
+
* fail on a timeout — catastrophic backtracking is SYNCHRONOUS, so it wedged
|
|
10121
|
+
* the vitest worker and the run never ended, which is a worse CI outcome than
|
|
10122
|
+
* the defect and indistinguishable from a slow machine. With the cap, dropping
|
|
10123
|
+
* the collapse turns that same input into a REFUSAL, so the behavior IS
|
|
10124
|
+
* observable through the public entry point. Asserting on the returned
|
|
10125
|
+
* pattern's shape is still the better fence — it names the invariant (how many
|
|
10126
|
+
* unknown spans survived) instead of a downstream consequence — but it is a
|
|
10127
|
+
* choice, not the only option.
|
|
10128
|
+
*/
|
|
10129
|
+
function intrinsicSkeletonPattern(source) {
|
|
10130
|
+
const keys = Object.keys(source);
|
|
10131
|
+
if (keys.length !== 1) return void 0;
|
|
10132
|
+
const key = keys[0];
|
|
10133
|
+
if (key === "Fn::Join") {
|
|
10134
|
+
const args = source[key];
|
|
10135
|
+
if (!Array.isArray(args) || args.length !== 2) return void 0;
|
|
10136
|
+
const [delimiter, parts] = args;
|
|
10137
|
+
if (typeof delimiter !== "string" || !Array.isArray(parts)) return void 0;
|
|
10138
|
+
const separator = escapeRegExp(delimiter);
|
|
10139
|
+
const segments = [];
|
|
10140
|
+
parts.forEach((part, index) => {
|
|
10141
|
+
if (index > 0) segments.push(separator);
|
|
10142
|
+
segments.push(typeof part === "string" ? escapeRegExp(part) : SKELETON_WILDCARD);
|
|
10143
|
+
});
|
|
10144
|
+
const body = joinSkeletonSegments(segments);
|
|
10145
|
+
return body === void 0 ? void 0 : new RegExp(`^${body}$`);
|
|
10146
|
+
}
|
|
10147
|
+
if (key === "Fn::Sub") {
|
|
10148
|
+
const args = source[key];
|
|
10149
|
+
const template = typeof args === "string" ? args : Array.isArray(args) ? args[0] : void 0;
|
|
10150
|
+
if (typeof template !== "string") return void 0;
|
|
10151
|
+
const segments = [];
|
|
10152
|
+
let cursor = 0;
|
|
10153
|
+
const variable = /\$\{([^}]*)\}/g;
|
|
10154
|
+
let hit;
|
|
10155
|
+
while ((hit = variable.exec(template)) !== null) {
|
|
10156
|
+
segments.push(escapeRegExp(template.slice(cursor, hit.index)));
|
|
10157
|
+
const inner = hit[1];
|
|
10158
|
+
segments.push(inner.startsWith("!") ? escapeRegExp(`\${${inner.slice(1)}}`) : SKELETON_WILDCARD);
|
|
10159
|
+
cursor = hit.index + hit[0].length;
|
|
10160
|
+
}
|
|
10161
|
+
segments.push(escapeRegExp(template.slice(cursor)));
|
|
10162
|
+
const body = joinSkeletonSegments(segments);
|
|
10163
|
+
return body === void 0 ? void 0 : new RegExp(`^${body}$`);
|
|
10164
|
+
}
|
|
10165
|
+
}
|
|
10166
|
+
/**
|
|
10167
|
+
* Position a leaf whose SOURCE is an intrinsic OBJECT, by matching the shape of
|
|
10168
|
+
* that intrinsic against the expressions this process recorded as secrets
|
|
10169
|
+
* (issue #1916).
|
|
10170
|
+
*
|
|
10171
|
+
* This is the residual {@link redactByPath} left behind. Its plain-string arm
|
|
10172
|
+
* persists the source leaf VERBATIM, which needs a source string to copy; when
|
|
10173
|
+
* the source leaf is an `Fn::Join` / `Fn::Sub` there is none, so the leaf fell
|
|
10174
|
+
* to the value scan — and the value map is keyed by the resolved PLAINTEXT, so
|
|
10175
|
+
* a colliding pair collapses there exactly as it did before #1904. That is the
|
|
10176
|
+
* DOMINANT CDK shape rather than an edge case: any secret reached through an L2
|
|
10177
|
+
* token renders the secret ARN as a `Ref`, hence an `Fn::Join`.
|
|
10178
|
+
*
|
|
10179
|
+
* Three conditions must ALL hold before an expression is persisted, and each
|
|
10180
|
+
* removes a different way of being wrong:
|
|
10181
|
+
*
|
|
10182
|
+
* 1. The bag leaf's WHOLE value is a recorded secret plaintext. The skeleton
|
|
10183
|
+
* describes ONE complete `{{resolve:...}}` token, so a leaf that merely
|
|
10184
|
+
* EMBEDS a secret (a join with surrounding text) is not this shape at all
|
|
10185
|
+
* and must keep going to the value scan, which rewrites just the substring.
|
|
10186
|
+
* 2. EXACTLY ONE candidate matches. Two matching candidates mean the skeleton
|
|
10187
|
+
* genuinely cannot separate them (`{Ref}` in the position that differs), and
|
|
10188
|
+
* guessing would be the collapse this fix exists to remove, one step over.
|
|
10189
|
+
* 3. The match is not DEMONSTRABLY another value's expression. The pass's own
|
|
10190
|
+
* map holds each surviving expression against the plaintext it resolved to,
|
|
10191
|
+
* so a candidate recorded there under a DIFFERENT plaintext is refused. That
|
|
10192
|
+
* is what fences a bag/source misalignment: a shape-plausible expression is
|
|
10193
|
+
* rejected outright when the pass can see it resolved to something else. A
|
|
10194
|
+
* candidate absent from that map is a collapsed LOSER, which is the case
|
|
10195
|
+
* this whole function exists to serve, so it is accepted.
|
|
10196
|
+
*
|
|
10197
|
+
* Every rejection degrades to the value scan, i.e. to today's behavior, so no
|
|
10198
|
+
* case gets worse than it is without this pass.
|
|
10199
|
+
*
|
|
10200
|
+
* One residual is worth naming rather than leaving to be rediscovered: a single
|
|
10201
|
+
* WRONG candidate can win only when this leaf's own expression is in NEITHER
|
|
10202
|
+
* store — which is exactly an `ssm` reference whose `Type` came back
|
|
10203
|
+
* unclassifiable (deliberately unpinned per #1901) and which then lost the
|
|
10204
|
+
* value collapse. `recordedSecretExpressions` is process-wide, so the winner
|
|
10205
|
+
* could in principle come from another resource, and condition 3 cannot refuse
|
|
10206
|
+
* one the pass never recorded.
|
|
10207
|
+
*
|
|
10208
|
+
* **What that costs is a WRONG REFERENCE in state, not merely a noisy diff.**
|
|
10209
|
+
* Persisting another leaf's expression is the pre-#1904 failure exactly:
|
|
10210
|
+
* `resolveReplayProps` RE-RESOLVES the persisted expression against AWS and
|
|
10211
|
+
* hands the result to `provider.update`, so a rollback replays the wrong secret
|
|
10212
|
+
* — immediately, if the two references already resolve to different values. It
|
|
10213
|
+
* is narrow (unclassifiable-`ssm` leaves only) but it is not cosmetic, and an
|
|
10214
|
+
* earlier draft of this paragraph called it "a spurious UPDATE rather than a
|
|
10215
|
+
* disclosure", which understated it.
|
|
10216
|
+
*
|
|
10217
|
+
* There is deliberately NO {@link isKnownSecretExpression} check here, and the
|
|
10218
|
+
* asymmetry with {@link redactByPath}'s plain-string arm is principled rather
|
|
10219
|
+
* than an oversight. That arm's candidate is the SOURCE LEAF itself — arbitrary
|
|
10220
|
+
* template text, which genuinely can be a PUBLIC ssm reference that must stay
|
|
10221
|
+
* resolved in state (issue #1901), so it has to be tested. Here the candidates
|
|
10222
|
+
* come only from {@link recordedSecretExpressions} and from the values of a
|
|
10223
|
+
* {@link RecordedSecretValues} map, both of which the resolver populates ONLY
|
|
10224
|
+
* on a proven-secret verdict — so the test could never answer `false`, and an
|
|
10225
|
+
* unfalsifiable guard reads as protection while fencing nothing. Widening
|
|
10226
|
+
* either candidate source is what would make it necessary again.
|
|
10227
|
+
*/
|
|
10228
|
+
function positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions) {
|
|
10229
|
+
if (bag === "" || !secrets.has(bag)) return void 0;
|
|
10230
|
+
const pattern = intrinsicSkeletonPattern(source);
|
|
10231
|
+
if (!pattern) return void 0;
|
|
10232
|
+
const CONFLICTING = Symbol("conflicting plaintext");
|
|
10233
|
+
const plaintextOf = /* @__PURE__ */ new Map();
|
|
10234
|
+
for (const [plaintext, expression] of secrets) plaintextOf.set(expression, plaintextOf.has(expression) ? CONFLICTING : plaintext);
|
|
10235
|
+
let matched;
|
|
10236
|
+
for (const candidate of /* @__PURE__ */ new Set([...secretExpressions, ...recordedSecretExpressions$1])) {
|
|
10237
|
+
if (candidate.length > MAX_SKELETON_CANDIDATE_LENGTH) return void 0;
|
|
10238
|
+
if (!pattern.test(candidate)) continue;
|
|
10239
|
+
const recordedPlaintext = plaintextOf.get(candidate);
|
|
10240
|
+
if (recordedPlaintext !== void 0 && recordedPlaintext !== bag) continue;
|
|
10241
|
+
if (matched !== void 0) return void 0;
|
|
10242
|
+
matched = candidate;
|
|
10243
|
+
}
|
|
10244
|
+
return matched;
|
|
10245
|
+
}
|
|
10246
|
+
/**
|
|
10247
|
+
* PATH-based redaction: walk `bag` alongside a SOURCE bag that still carries the
|
|
10248
|
+
* unresolved `{{resolve:...}}` expressions, and wherever the source leaf is such
|
|
10249
|
+
* a string, persist THAT string verbatim.
|
|
10250
|
+
*
|
|
10251
|
+
* This exists because value-keyed redaction cannot answer the question at all
|
|
10252
|
+
* when two expressions share one resolved value (issue #1904): the map is keyed
|
|
10253
|
+
* by the plaintext, so the two collapse and every site is rewritten to whichever
|
|
10254
|
+
* expression was recorded last — state then holds an expression the template
|
|
10255
|
+
* does not have at that leaf, and the stack takes a permanent spurious UPDATE.
|
|
10256
|
+
* Position is the only disambiguator, and the source bag supplies it.
|
|
10257
|
+
*
|
|
10258
|
+
* It also covers the case where there is no secrets map to consult at all
|
|
10259
|
+
* (issue #1900): an UNCHANGED resource is never resolved during a deploy, so its
|
|
10260
|
+
* `perResourceSecrets` entry is empty, and a live readback that echoes a secret
|
|
10261
|
+
* would be persisted in plaintext. Projecting from the resource's OWN state
|
|
10262
|
+
* record — which already holds the expressions — redacts it with no secret
|
|
10263
|
+
* fetch and no value matching.
|
|
10264
|
+
*
|
|
10265
|
+
* A source leaf that is an intrinsic OBJECT (`Fn::Join` / `Fn::Sub`) has no
|
|
10266
|
+
* string to copy, so it goes through {@link positionByIntrinsicSkeleton} first
|
|
10267
|
+
* (issue #1916): when the intrinsic's literal parts describe exactly one of the
|
|
10268
|
+
* recorded secret expressions, THAT is persisted. This is the dominant CDK
|
|
10269
|
+
* shape — an L2 secret token renders the ARN as a `Ref`, hence a join.
|
|
10270
|
+
*
|
|
10271
|
+
* The value scan is still applied wherever neither can answer: a leaf that
|
|
10272
|
+
* merely EMBEDS a secret inside surrounding text, an intrinsic whose skeleton
|
|
10273
|
+
* matches zero or several candidates, a diverged shape, a key the source lacks.
|
|
10274
|
+
* So the passes are complementary rather than alternatives — path where
|
|
10275
|
+
* position is knowable, skeleton where the position is an intrinsic, value
|
|
10276
|
+
* where neither is.
|
|
10277
|
+
*/
|
|
10278
|
+
function redactByPath(bag, source, secrets, rules, secretExpressions) {
|
|
10279
|
+
if (typeof bag === "string" && isSingleDynamicReferenceToken(bag)) return bag;
|
|
10280
|
+
if (isDynamicReferenceString(source) && typeof bag === "string") {
|
|
10281
|
+
if (isSingleDynamicReferenceToken(source) && (rules.trustAnyExpression || isKnownSecretExpression(source, secretExpressions))) return source;
|
|
10282
|
+
return redactSecretsForState(bag, secrets);
|
|
10283
|
+
}
|
|
10284
|
+
if (typeof bag === "string" && isPlainObject$2(source)) {
|
|
10285
|
+
const positioned = positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions);
|
|
10286
|
+
if (positioned !== void 0) return positioned;
|
|
10287
|
+
}
|
|
10288
|
+
if (rules.descendArrays && Array.isArray(bag) && Array.isArray(source) && bag.length === source.length) return bag.map((item, i) => redactByPath(item, source[i], secrets, rules, secretExpressions));
|
|
10289
|
+
if (isPlainObject$2(bag) && isPlainObject$2(source)) {
|
|
10290
|
+
const out = {};
|
|
10291
|
+
for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? redactByPath(v, source[k], secrets, rules, secretExpressions) : redactSecretsForState(v, secrets);
|
|
10292
|
+
return out;
|
|
10293
|
+
}
|
|
10294
|
+
return redactSecretsForState(bag, secrets);
|
|
10295
|
+
}
|
|
9845
10296
|
/**
|
|
9846
10297
|
* Deep-clone `bag`, replacing every occurrence of a recorded secret value with
|
|
9847
10298
|
* the unresolved `{{resolve:...}}` expression it came from. A string whose WHOLE
|
|
@@ -9851,8 +10302,9 @@ function buildNeedleRegex(values) {
|
|
|
9851
10302
|
* is nothing to redact, so callers can persist the original object unchanged in
|
|
9852
10303
|
* the common no-secret case.
|
|
9853
10304
|
*/
|
|
9854
|
-
function redactSecretsForState(bag, secrets) {
|
|
9855
|
-
if (secrets.size === 0) return bag;
|
|
10305
|
+
function redactSecretsForState(bag, secrets, source, rules = TEMPLATE_DERIVED_RULES) {
|
|
10306
|
+
if (secrets.size === 0 && source === void 0) return bag;
|
|
10307
|
+
if (source !== void 0) return redactByPath(bag, source, secrets, rules, new Set(secrets.values()));
|
|
9856
10308
|
const regex = buildNeedleRegex(secrets.keys());
|
|
9857
10309
|
const wholeValueExpr = (s) => s === "" ? void 0 : secrets.get(s);
|
|
9858
10310
|
const walk = (value) => {
|
|
@@ -9883,12 +10335,12 @@ function redactSecretsForState(bag, secrets) {
|
|
|
9883
10335
|
* detect a no-op cheaply. Shared by the deploy engine's save choke point and
|
|
9884
10336
|
* the `cdkd scrub` command so both scrub the same three fields identically.
|
|
9885
10337
|
*/
|
|
9886
|
-
function scrubResourceRecord(record, secrets) {
|
|
9887
|
-
if (secrets.size === 0) return record;
|
|
10338
|
+
function scrubResourceRecord(record, secrets, sourceProperties) {
|
|
10339
|
+
if (secrets.size === 0 && sourceProperties === void 0 && !record.observedProperties) return record;
|
|
9888
10340
|
const next = { ...record };
|
|
9889
|
-
next.properties = redactSecretsForState(record.properties, secrets);
|
|
10341
|
+
next.properties = redactSecretsForState(record.properties, secrets, sourceProperties);
|
|
9890
10342
|
if (record.attributes) next.attributes = redactSecretsForState(record.attributes, secrets);
|
|
9891
|
-
if (record.observedProperties) next.observedProperties = redactSecretsForState(record.observedProperties, secrets);
|
|
10343
|
+
if (record.observedProperties) next.observedProperties = redactSecretsForState(record.observedProperties, secrets, sourceProperties ?? next.properties, sourceProperties === void 0 ? STATE_SOURCED_READBACK_RULES : TEMPLATE_SOURCED_READBACK_RULES);
|
|
9892
10344
|
return next;
|
|
9893
10345
|
}
|
|
9894
10346
|
/**
|
|
@@ -11494,28 +11946,51 @@ const cachedAvailabilityZones = {};
|
|
|
11494
11946
|
*/
|
|
11495
11947
|
const cachedDynamicReferences = {};
|
|
11496
11948
|
/**
|
|
11497
|
-
* The `{{resolve
|
|
11498
|
-
*
|
|
11499
|
-
*
|
|
11500
|
-
*
|
|
11501
|
-
*
|
|
11502
|
-
*
|
|
11503
|
-
*
|
|
11504
|
-
*
|
|
11505
|
-
*
|
|
11506
|
-
*
|
|
11507
|
-
*
|
|
11508
|
-
*
|
|
11509
|
-
*
|
|
11510
|
-
*
|
|
11511
|
-
*
|
|
11512
|
-
*
|
|
11513
|
-
*
|
|
11514
|
-
* Only the TYPE is remembered, never the decrypted value:
|
|
11515
|
-
* lookup is made with `WithDecryption: false`, so the
|
|
11516
|
-
* fetched at all there.
|
|
11517
|
-
|
|
11518
|
-
|
|
11949
|
+
* The `{{resolve:...}}` expressions this process has PROVEN resolve to a
|
|
11950
|
+
* SECRET, reached through `secret-redaction.ts`'s `recordedSecretExpressions`
|
|
11951
|
+
* store. Lifetime is {@link cachedDynamicReferences}'s — `resetAccountInfoCache`
|
|
11952
|
+
* clears both, so a stale verdict can never decide secret-ness for a reference
|
|
11953
|
+
* whose resolved value that call just asked to forget.
|
|
11954
|
+
*
|
|
11955
|
+
* `ssm` is the kind that NEEDS the memory (issue #1901). A plain `ssm`
|
|
11956
|
+
* reference is not a secret by SPELLING the way `secretsmanager` is — whether
|
|
11957
|
+
* it resolves to public config or to a decrypted secret depends on the
|
|
11958
|
+
* parameter's `Type`, which is only knowable from the `GetParameter` response.
|
|
11959
|
+
* So secret-ness is discovered on the first resolution and remembered, keyed by
|
|
11960
|
+
* the full `{{resolve:...}}` expression. Two consumers need it AFTER the lookup
|
|
11961
|
+
* that populated it: the cache-hit arm (which must re-record the value as a
|
|
11962
|
+
* secret for the current resolution pass) and the diff / no-op path (which must
|
|
11963
|
+
* leave a SecureString reference unresolved without paying a lookup at all once
|
|
11964
|
+
* the type is known). A reference NOT in the set is only "not known to be
|
|
11965
|
+
* secure" — never "proven public" — so every arm that would leak still asks AWS
|
|
11966
|
+
* for the type first. Only the TYPE is remembered, never the decrypted value:
|
|
11967
|
+
* on the diff path the lookup is made with `WithDecryption: false`, so the
|
|
11968
|
+
* plaintext is never fetched at all there.
|
|
11969
|
+
*
|
|
11970
|
+
* A `secretsmanager` reference is recorded TOO, and that is issue
|
|
11971
|
+
* [#1916](https://github.com/go-to-k/cdkd/issues/1916). It needed no memory
|
|
11972
|
+
* while the only question asked of the set was "is this expression secret?",
|
|
11973
|
+
* which its spelling settles — but the set is ALSO the candidate list the
|
|
11974
|
+
* redaction path matches an INTRINSIC source leaf against
|
|
11975
|
+
* ({@link secret-redaction.redactSecretsForState}), and a list holding only the
|
|
11976
|
+
* ssm half cannot name the losing member of a collapsed
|
|
11977
|
+
* secretsmanager/secretsmanager pair. Recording every kind is what makes the
|
|
11978
|
+
* set mean what its name says. It changes no verdict here: every arm that reads
|
|
11979
|
+
* it for secret-ness already answers `true` on a `secretsmanager` spelling
|
|
11980
|
+
* before consulting it.
|
|
11981
|
+
*
|
|
11982
|
+
* The store lives in `secret-redaction.ts` rather than here (issue
|
|
11983
|
+
* [#1910](https://github.com/go-to-k/cdkd/issues/1910)) because the redaction
|
|
11984
|
+
* path is the other consumer: one set in the LEAF module means the redactor can
|
|
11985
|
+
* answer with no caller threading it, and the resolver reaches it along an
|
|
11986
|
+
* import edge it already has — the reverse would close a cycle.
|
|
11987
|
+
*/
|
|
11988
|
+
const recordedSecretExpressions = {
|
|
11989
|
+
has: (expression) => isRecordedSecretExpression(expression),
|
|
11990
|
+
add: (expression) => recordSecretExpression(expression),
|
|
11991
|
+
delete: (expression) => forgetSecretExpression(expression),
|
|
11992
|
+
clear: () => clearRecordedSecretExpressions()
|
|
11993
|
+
};
|
|
11519
11994
|
/**
|
|
11520
11995
|
* Cache for EC2 instance attributes that require a live DescribeInstances
|
|
11521
11996
|
* lookup (PrivateIp / PublicIp / PrivateDnsName / PublicDnsName /
|
|
@@ -13327,7 +13802,7 @@ var IntrinsicFunctionResolver = class {
|
|
|
13327
13802
|
});
|
|
13328
13803
|
for (const { fullMatch, inner } of matches) {
|
|
13329
13804
|
const service = inner.split(":")[0];
|
|
13330
|
-
const isKnownSecret = service === "secretsmanager" ||
|
|
13805
|
+
const isKnownSecret = service === "secretsmanager" || recordedSecretExpressions.has(fullMatch);
|
|
13331
13806
|
if (isKnownSecret && context?.skipDynamicReferences) continue;
|
|
13332
13807
|
if (fullMatch in cachedDynamicReferences) {
|
|
13333
13808
|
const cached = cachedDynamicReferences[fullMatch];
|
|
@@ -13342,8 +13817,8 @@ var IntrinsicFunctionResolver = class {
|
|
|
13342
13817
|
else if (service === "ssm") {
|
|
13343
13818
|
const decrypt = context?.skipDynamicReferences !== true;
|
|
13344
13819
|
const param = await this.resolveSSMReference(parts, decrypt);
|
|
13345
|
-
if (param.type === "SecureString")
|
|
13346
|
-
else if (!param.secure)
|
|
13820
|
+
if (param.type === "SecureString") recordedSecretExpressions.add(fullMatch);
|
|
13821
|
+
else if (!param.secure) recordedSecretExpressions.delete(fullMatch);
|
|
13347
13822
|
isSecret = param.secure;
|
|
13348
13823
|
if (param.secure) {
|
|
13349
13824
|
if (!decrypt) continue;
|
|
@@ -13354,7 +13829,10 @@ var IntrinsicFunctionResolver = class {
|
|
|
13354
13829
|
continue;
|
|
13355
13830
|
}
|
|
13356
13831
|
cachedDynamicReferences[fullMatch] = resolved;
|
|
13357
|
-
if (isSecret && resolved)
|
|
13832
|
+
if (isSecret && resolved) {
|
|
13833
|
+
context?.recordedSecretValues?.set(resolved, fullMatch);
|
|
13834
|
+
if (service === "secretsmanager") recordedSecretExpressions.add(fullMatch);
|
|
13835
|
+
}
|
|
13358
13836
|
result = result.replace(fullMatch, () => resolved);
|
|
13359
13837
|
}
|
|
13360
13838
|
return result;
|
|
@@ -14409,7 +14887,7 @@ var CloudControlProvider = class {
|
|
|
14409
14887
|
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);
|
|
14410
14888
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
14411
14889
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
14412
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
14890
|
+
const { ASGProvider } = await import("./asg-provider-BqKgMaXN.js").then((n) => n.n);
|
|
14413
14891
|
return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
|
|
14414
14892
|
}
|
|
14415
14893
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -21030,10 +21508,35 @@ async function resolveReplayProps(props, resolver, secrets) {
|
|
|
21030
21508
|
* (GHSA fix). The record's `properties` may be the provider's
|
|
21031
21509
|
* `effectiveProperties`, which can echo the value we just resolved for the
|
|
21032
21510
|
* provider call — so scrub it with the same per-op secrets map before it is
|
|
21033
|
-
* persisted.
|
|
21034
|
-
|
|
21035
|
-
|
|
21036
|
-
|
|
21511
|
+
* persisted.
|
|
21512
|
+
*
|
|
21513
|
+
* `journaledProps` is the POSITION source (issue #1910): the JOURNALED previous
|
|
21514
|
+
* properties, whose leaves still carry the unresolved `{{resolve:...}}`
|
|
21515
|
+
* expressions this replay resolved FROM. Without it two expressions sharing one
|
|
21516
|
+
* resolved value collapse onto whichever the replay recorded last, so the state
|
|
21517
|
+
* this rollback writes disagrees with the template at one leaf and the next
|
|
21518
|
+
* deploy reports a change that never converges — the same defect the four
|
|
21519
|
+
* deploy-side writers had, arriving here through the replay instead.
|
|
21520
|
+
*
|
|
21521
|
+
* It takes `STATE_DERIVED_RULES`, which is BOTH relaxations. The source is a
|
|
21522
|
+
* persisted record, so it holds no PUBLIC expressions (a `String` ssm reference
|
|
21523
|
+
* is stored resolved) and any `{{resolve:...}}` in it is by construction a
|
|
21524
|
+
* secret — that is `trustAnyExpression`. And the bag WAS produced by resolving
|
|
21525
|
+
* that source (`resolveReplayProps` -> the provider's `effectiveProperties`),
|
|
21526
|
+
* so the two have identical structure and positional array descent is sound —
|
|
21527
|
+
* that is `descendArrays`. Using `STATE_SOURCED_READBACK_RULES` here reads
|
|
21528
|
+
* plausible and is wrong in the quiet direction: it turns array descent off, so
|
|
21529
|
+
* a secret nested in a `Tags[]` / ECS `secrets[]` element would still collapse
|
|
21530
|
+
* on the replay path.
|
|
21531
|
+
*
|
|
21532
|
+
* No-op when the op resolved no secret.
|
|
21533
|
+
*/
|
|
21534
|
+
function redactRollbackRecord(record, secrets, journaledProps) {
|
|
21535
|
+
if (secrets.size === 0) return record;
|
|
21536
|
+
return scrubResourceRecord(journaledProps === void 0 ? record : {
|
|
21537
|
+
...record,
|
|
21538
|
+
properties: redactSecretsForState(record.properties, secrets, journaledProps, STATE_DERIVED_RULES)
|
|
21539
|
+
}, secrets);
|
|
21037
21540
|
}
|
|
21038
21541
|
async function updateWithRollbackRetry(provider, args, logicalId, logger, isInterrupted) {
|
|
21039
21542
|
if (provider.disableOuterRetry) return await provider.update(...args);
|
|
@@ -21279,7 +21782,7 @@ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds
|
|
|
21279
21782
|
physicalId: createResult.physicalId,
|
|
21280
21783
|
attributes: createResult.attributes ?? {},
|
|
21281
21784
|
properties: recordedPropertiesAfterReplayCreate(prevRecord, createResult)
|
|
21282
|
-
}, secrets);
|
|
21785
|
+
}, secrets, prevRecord.properties);
|
|
21283
21786
|
await afterOp?.(op.logicalId);
|
|
21284
21787
|
if (!deletedNewFirst && !adoptedLiveNewResource) try {
|
|
21285
21788
|
const finalSnapshotIdentifier = rollbackFinalSnapshotId(op.resourceType, current, op.provisionedBy);
|
|
@@ -21330,7 +21833,7 @@ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds
|
|
|
21330
21833
|
desiredProps ?? {},
|
|
21331
21834
|
currentProps ?? {}
|
|
21332
21835
|
], op.logicalId, logger, isInterrupted);
|
|
21333
|
-
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(previousState, revertResult), secrets);
|
|
21836
|
+
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(previousState, revertResult), secrets, previousState.properties);
|
|
21334
21837
|
logger.info(` Rollback: ${op.logicalId} restored successfully`);
|
|
21335
21838
|
await afterOp?.(op.logicalId);
|
|
21336
21839
|
ctx.recordEvent?.({
|
|
@@ -21470,7 +21973,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
21470
21973
|
desiredProps ?? {},
|
|
21471
21974
|
attemptedProps ?? {}
|
|
21472
21975
|
], op.logicalId, logger, options.isInterrupted);
|
|
21473
|
-
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(prev, revertFailedResult), secrets);
|
|
21976
|
+
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(prev, revertFailedResult), secrets, prev.properties);
|
|
21474
21977
|
logger.info(` Rollback: ${op.logicalId} reverted successfully`);
|
|
21475
21978
|
await options.afterOp?.(op.logicalId);
|
|
21476
21979
|
ctx.recordEvent?.({
|
|
@@ -21596,7 +22099,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
21596
22099
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
21597
22100
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
21598
22101
|
function getCdkdVersion() {
|
|
21599
|
-
return "0.283.
|
|
22102
|
+
return "0.283.25";
|
|
21600
22103
|
}
|
|
21601
22104
|
/**
|
|
21602
22105
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -22210,6 +22713,17 @@ var DeployEngine = class {
|
|
|
22210
22713
|
*/
|
|
22211
22714
|
perResourceSecrets = /* @__PURE__ */ new Map();
|
|
22212
22715
|
/**
|
|
22716
|
+
* PER-RESOURCE unresolved TEMPLATE properties, keyed by logicalId (issues
|
|
22717
|
+
* #1904 / #1900). The redaction choke point uses this as the POSITION source:
|
|
22718
|
+
* wherever the template leaf is a `{{resolve:...}}` string, state persists
|
|
22719
|
+
* that string verbatim instead of asking the value-keyed map which expression
|
|
22720
|
+
* a plaintext came from — a question that map cannot answer when two
|
|
22721
|
+
* expressions resolve to the same value. Captured at the same two sites that
|
|
22722
|
+
* populate `perResourceSecrets`, where the unresolved bag is already in hand.
|
|
22723
|
+
* Reset per `deploy()`.
|
|
22724
|
+
*/
|
|
22725
|
+
perResourceTemplateProps = /* @__PURE__ */ new Map();
|
|
22726
|
+
/**
|
|
22213
22727
|
* Resolved secrets recorded while resolving the stack OUTPUTS (a `CfnOutput`
|
|
22214
22728
|
* whose Value resolves a `{{resolve:...}}` reference). Separate from the
|
|
22215
22729
|
* per-resource maps for the same anti-cross-contamination reason. Reset per
|
|
@@ -22217,6 +22731,15 @@ var DeployEngine = class {
|
|
|
22217
22731
|
*/
|
|
22218
22732
|
outputSecrets = /* @__PURE__ */ new Map();
|
|
22219
22733
|
/**
|
|
22734
|
+
* UNRESOLVED template `Outputs` values, keyed by output name (issue #1910) —
|
|
22735
|
+
* the outputs' POSITION source, the sibling of `perResourceTemplateProps` for
|
|
22736
|
+
* the bag `resolveOutputs` produces. Without it two outputs resolving one
|
|
22737
|
+
* secret collapse onto whichever expression was recorded last, exactly as two
|
|
22738
|
+
* resource properties did before #1904. Captured in `resolveOutputs` at the
|
|
22739
|
+
* same point `outputSecrets` is accumulated. Reset per `deploy()`.
|
|
22740
|
+
*/
|
|
22741
|
+
outputsTemplateSource = {};
|
|
22742
|
+
/**
|
|
22220
22743
|
* Per-logical-id snapshot of the intrinsic-RESOLVED desired properties
|
|
22221
22744
|
* each CREATE / UPDATE attempted (issue #1198). Written just before the
|
|
22222
22745
|
* provider call; read only when the op FAILS, to journal the failed op's
|
|
@@ -22258,7 +22781,9 @@ var DeployEngine = class {
|
|
|
22258
22781
|
this.recordedImports = [];
|
|
22259
22782
|
this.recordedOutputReads = [];
|
|
22260
22783
|
this.perResourceSecrets = /* @__PURE__ */ new Map();
|
|
22784
|
+
this.perResourceTemplateProps = /* @__PURE__ */ new Map();
|
|
22261
22785
|
this.outputSecrets = /* @__PURE__ */ new Map();
|
|
22786
|
+
this.outputsTemplateSource = {};
|
|
22262
22787
|
this.resolver.resetPhysicalIdFallbackCount();
|
|
22263
22788
|
return withStackName(stackName, () => this.doDeploy(stackName, template));
|
|
22264
22789
|
}
|
|
@@ -22290,13 +22815,19 @@ var DeployEngine = class {
|
|
|
22290
22815
|
* un-redacted resolved bag; only the persisted copy is rewritten.
|
|
22291
22816
|
*/
|
|
22292
22817
|
/**
|
|
22293
|
-
*
|
|
22294
|
-
*
|
|
22295
|
-
*
|
|
22818
|
+
* Redact the resolved stack OUTPUTS bag, positioned by the unresolved
|
|
22819
|
+
* template `Outputs` values (issue #1910).
|
|
22820
|
+
*
|
|
22821
|
+
* A single entry point because THREE call sites redact this same bag — the
|
|
22822
|
+
* state-persist choke point, the no-change re-check, and the post-deploy
|
|
22823
|
+
* publish that feeds the exports index / deploy summary — and before this
|
|
22824
|
+
* they each spelled the value-only redaction separately. Two outputs
|
|
22825
|
+
* resolving one secret collapsed onto whichever expression was recorded last
|
|
22826
|
+
* at all three.
|
|
22296
22827
|
*/
|
|
22297
|
-
|
|
22298
|
-
if (
|
|
22299
|
-
return redactSecretsForState(
|
|
22828
|
+
redactOutputs(outputs) {
|
|
22829
|
+
if (this.outputSecrets.size === 0) return outputs;
|
|
22830
|
+
return redactSecretsForState(outputs, this.outputSecrets, this.outputsTemplateSource);
|
|
22300
22831
|
}
|
|
22301
22832
|
/**
|
|
22302
22833
|
* Redact resolved secret plaintext out of rollback-journal operations (GHSA
|
|
@@ -22307,21 +22838,34 @@ var DeployEngine = class {
|
|
|
22307
22838
|
* whole-secret value from one resource cannot rewrite another's literal.
|
|
22308
22839
|
* Preserves ops that carry none of those fields, or whose resource recorded
|
|
22309
22840
|
* no secret.
|
|
22841
|
+
*
|
|
22842
|
+
* The POSITION source matters MORE here than anywhere else (issue #1910).
|
|
22843
|
+
* This journal is not just persisted, it is REPLAYED to AWS by the rollback
|
|
22844
|
+
* executor's `resolveReplayProps`, so a leaf redacted onto a SIBLING's
|
|
22845
|
+
* expression — which is what the value-keyed map does when two expressions
|
|
22846
|
+
* resolve to one value — re-resolves at replay time to the wrong reference.
|
|
22847
|
+
* For two `:AWSCURRENT` / `:AWSPREVIOUS` stages of one secret that is the
|
|
22848
|
+
* wrong VERSION shipped to the live resource the moment the two diverge.
|
|
22849
|
+
*
|
|
22850
|
+
* The two SIDES take different sources, and conflating them would be a fresh
|
|
22851
|
+
* defect rather than a simplification: `properties` / `attemptedProperties`
|
|
22852
|
+
* are this deploy's DESIRED bags, produced by resolving the CURRENT template,
|
|
22853
|
+
* so the template bag positions them. `previousState` is a record read back
|
|
22854
|
+
* from STATE, whose leaves already hold expressions from whenever it was
|
|
22855
|
+
* written — the current template is not its source and may not even have the
|
|
22856
|
+
* same shape — so it is redacted against ITSELF via `scrubResourceRecord`,
|
|
22857
|
+
* the same #1900 fallback an UNCHANGED resource takes.
|
|
22310
22858
|
*/
|
|
22311
22859
|
redactOperationsForJournal(operations) {
|
|
22312
22860
|
return operations.map((op) => {
|
|
22313
22861
|
const secrets = this.perResourceSecrets.get(op.logicalId);
|
|
22314
|
-
|
|
22862
|
+
const templateProps = this.perResourceTemplateProps.get(op.logicalId);
|
|
22863
|
+
if ((!secrets || secrets.size === 0) && !op.previousState) return op;
|
|
22864
|
+
const ownSecrets = secrets ?? /* @__PURE__ */ new Map();
|
|
22315
22865
|
const next = { ...op };
|
|
22316
|
-
if (next.properties) next.properties = redactSecretsForState(next.properties,
|
|
22317
|
-
if (next.attemptedProperties) next.attemptedProperties = redactSecretsForState(next.attemptedProperties,
|
|
22318
|
-
if (next.previousState)
|
|
22319
|
-
const prev = { ...next.previousState };
|
|
22320
|
-
prev.properties = redactSecretsForState(prev.properties, secrets);
|
|
22321
|
-
if (prev.attributes) prev.attributes = redactSecretsForState(prev.attributes, secrets);
|
|
22322
|
-
if (prev.observedProperties) prev.observedProperties = redactSecretsForState(prev.observedProperties, secrets);
|
|
22323
|
-
next.previousState = prev;
|
|
22324
|
-
}
|
|
22866
|
+
if (next.properties) next.properties = redactSecretsForState(next.properties, ownSecrets, templateProps);
|
|
22867
|
+
if (next.attemptedProperties) next.attemptedProperties = redactSecretsForState(next.attemptedProperties, ownSecrets, templateProps);
|
|
22868
|
+
if (next.previousState) next.previousState = scrubResourceRecord(next.previousState, ownSecrets);
|
|
22325
22869
|
return next;
|
|
22326
22870
|
});
|
|
22327
22871
|
}
|
|
@@ -22329,12 +22873,13 @@ var DeployEngine = class {
|
|
|
22329
22873
|
const resources = {};
|
|
22330
22874
|
for (const [logicalId, record] of Object.entries(state.resources)) {
|
|
22331
22875
|
const secrets = this.perResourceSecrets.get(logicalId);
|
|
22332
|
-
|
|
22876
|
+
const templateProps = this.perResourceTemplateProps.get(logicalId);
|
|
22877
|
+
resources[logicalId] = scrubResourceRecord(record, secrets ?? /* @__PURE__ */ new Map(), templateProps);
|
|
22333
22878
|
}
|
|
22334
22879
|
return {
|
|
22335
22880
|
...state,
|
|
22336
22881
|
resources,
|
|
22337
|
-
outputs: this.
|
|
22882
|
+
outputs: this.redactOutputs(state.outputs)
|
|
22338
22883
|
};
|
|
22339
22884
|
}
|
|
22340
22885
|
/**
|
|
@@ -22619,7 +23164,7 @@ var DeployEngine = class {
|
|
|
22619
23164
|
this.logger.info("No changes detected. Stack is up to date.");
|
|
22620
23165
|
let persistedOutputs = currentState.outputs ?? {};
|
|
22621
23166
|
if (!this.options.dryRun) {
|
|
22622
|
-
const resolvedOutputs = this.
|
|
23167
|
+
const resolvedOutputs = this.redactOutputs(await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, parameterValues, conditions));
|
|
22623
23168
|
const resolutionFailed = Object.values(resolvedOutputs).some((v) => v === void 0);
|
|
22624
23169
|
const outputsChanged = !resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs);
|
|
22625
23170
|
if (resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs)) this.logger.warn("Outputs changed but one or more could not be resolved; keeping the previously persisted outputs. A downstream Fn::ImportValue may fail until the next deploy.");
|
|
@@ -22947,7 +23492,7 @@ var DeployEngine = class {
|
|
|
22947
23492
|
let outputs;
|
|
22948
23493
|
try {
|
|
22949
23494
|
outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
|
|
22950
|
-
outputs = this.
|
|
23495
|
+
outputs = this.redactOutputs(outputs);
|
|
22951
23496
|
} catch (outputError) {
|
|
22952
23497
|
await this.persistStateAfterOutputFailure(stackName, currentState, newResources, currentEtag, pendingMigration);
|
|
22953
23498
|
await this.writeRollbackJournalSegment(stackName, completedOperations, failedOperations, "no-rollback-failure", currentEtag === void 0);
|
|
@@ -23343,6 +23888,7 @@ var DeployEngine = class {
|
|
|
23343
23888
|
...conditions && { conditions }
|
|
23344
23889
|
}, stackName);
|
|
23345
23890
|
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
23891
|
+
this.perResourceTemplateProps.set(logicalId, desiredProps);
|
|
23346
23892
|
if (context.recordedSecretValues) this.perResourceSecrets.set(logicalId, context.recordedSecretValues);
|
|
23347
23893
|
this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
|
|
23348
23894
|
this.attemptedResolvedProps.set(logicalId, resolvedProps);
|
|
@@ -23387,9 +23933,10 @@ var DeployEngine = class {
|
|
|
23387
23933
|
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
23388
23934
|
const updateSecrets = context.recordedSecretValues ?? /* @__PURE__ */ new Map();
|
|
23389
23935
|
this.perResourceSecrets.set(logicalId, updateSecrets);
|
|
23936
|
+
this.perResourceTemplateProps.set(logicalId, desiredProps);
|
|
23390
23937
|
this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
|
|
23391
23938
|
this.attemptedResolvedProps.set(logicalId, resolvedProps);
|
|
23392
|
-
if (JSON.stringify(
|
|
23939
|
+
if (JSON.stringify(redactSecretsForState(resolvedProps, updateSecrets, desiredProps)) === JSON.stringify(currentProps)) {
|
|
23393
23940
|
if (change.attributeChanges && change.attributeChanges.length > 0) {
|
|
23394
23941
|
const attrSummary = change.attributeChanges.map((a) => `${a.attribute}: ${a.oldValue ?? "(unset)"} → ${a.newValue ?? "(unset)"}`).join(", ");
|
|
23395
23942
|
this.logger.info(` ↻ ${logicalId} (${resourceType}) attribute update: ${attrSummary}`);
|
|
@@ -23907,7 +24454,10 @@ var DeployEngine = class {
|
|
|
23907
24454
|
outputs[outputKey] = value;
|
|
23908
24455
|
if (output.Export?.Name) {
|
|
23909
24456
|
const exportName = typeof output.Export.Name === "string" ? output.Export.Name : await this.resolver.resolve(output.Export.Name, context);
|
|
23910
|
-
if (typeof exportName === "string")
|
|
24457
|
+
if (typeof exportName === "string") {
|
|
24458
|
+
outputs[exportName] = value;
|
|
24459
|
+
this.outputsTemplateSource[exportName] = output.Value;
|
|
24460
|
+
}
|
|
23911
24461
|
}
|
|
23912
24462
|
} catch (error) {
|
|
23913
24463
|
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 });
|
|
@@ -23916,6 +24466,7 @@ var DeployEngine = class {
|
|
|
23916
24466
|
}
|
|
23917
24467
|
}
|
|
23918
24468
|
if (context.recordedSecretValues) for (const [value, expr] of context.recordedSecretValues) this.outputSecrets.set(value, expr);
|
|
24469
|
+
for (const [outputKey, output] of Object.entries(template.Outputs)) this.outputsTemplateSource[outputKey] = output.Value;
|
|
23919
24470
|
return outputs;
|
|
23920
24471
|
}
|
|
23921
24472
|
buildDisplayOutputs(template, resolvedOutputs) {
|
|
@@ -23930,5 +24481,5 @@ var DeployEngine = class {
|
|
|
23930
24481
|
};
|
|
23931
24482
|
|
|
23932
24483
|
//#endregion
|
|
23933
|
-
export { coerceCfnBoolean as $,
|
|
23934
|
-
//# sourceMappingURL=deploy-engine-
|
|
24484
|
+
export { coerceCfnBoolean as $, resolveAutoAssetStorage as $t, cyan as A, LocalMigrateError as An, loadPublishableAssetManifest as At, findSilentDropProperties as B, StackTerminationProtectionError as Bn, buildDenyExternalAccessPolicy as Bt, makeCanonicalizePropertiesFn as C, setAwsClients as Cn, rebuildClientForBucketRegion as Ct, renderStatefulReason as D, DependencyError as Dn, WorkGraph as Dt, isStatefulRecreateTargetSync as E, ConfigError as En, stringifyValue as Et, IAMRoleProvider as F, PartialFailureError as Fn, ensureAssetStorage as Ft, IntrinsicFunctionResolver as G, normalizeAwsError as Gn, runDockerStreaming as Gt, slowCcOperationTimeoutMs as H, SynthesisError as Hn, formatDockerLoginError as Ht, collectInlinePolicyNamesManagedBySiblings as I, ProvisioningError as In, getBootstrapMarkerKey as It, refStateLookupFromResource as J, isRetryableTransientError as Jn, Synthesizer as Jt, cfnRefValueFromPhysicalId as K, withErrorHandling as Kn, AssetManifestLoader as Kt, clearOnUpdateRemoval as L, ResourceTimeoutError as Ln, parseBootstrapMarker as Lt, green as M, LockError as Mn, escapeRegExp$1 as Mt, red as N, MissingCdkCliError as Nn, AssetModeResolver as Nt, formatResourceLine as O, DeployCancelledError as On, buildAssetRedirectMap as Ot, yellow as P, NestedStackChildDirectDestroyError as Pn, BOOTSTRAP_MARKER_PREFIX as Pt, assertRegionMatch as Q, resolveApp as Qt, ProviderRegistry as R, ResourceUpdateNotSupportedError as Rn, validateAssetBucketName as Rt, unsupportedFinalSnapshotError as S, resetAwsClients as Sn, S3StateBackend as St, MULTI_REGION_RECREATE_BLOCKED_TYPES as T, CdkdError as Tn, AssetPublisher as Tt, disableInstanceApiTermination as U, formatError as Un, getDockerCmd as Ut, CloudControlProvider as V, StateError as Vn, buildDockerImage as Vt, isTerminationProtectionPropagationError as W, isCdkdError as Wn, runDockerForeground as Wt, normalizeAwsTagsToCfn as X, markNonRetryable as Xn, getDefaultStateBucketName as Xt, WAFv2WebACLProvider as Y, isThrottlingError as Yn, synthesisStatusMessage as Yt, resolveExplicitPhysicalId as Z, __exportAll as Zn, getLegacyStateBucketName as Zt, buildFinalSnapshotIdentifier as _, processStackMessages as _n, describeTypeWithThrottleRetry as _t, DeploymentEventsStore as a, stateBucketExistenceConfirmed as an, requireConfigObject as at, isFinalSnapshotError as b, AwsClients as bn, TemplateParser as bt, replayFailedOperations as c, CFN_TEMPLATE_URL_LIMIT as cn, redactSecretsForState as ct, deleteSkipReason as d, uploadCfnTemplate as dn, s3BucketDomainName as dt, resolveCaptureObservedState as en, configBooleanRefusal as et, withResourceDeadline as f, expectedOwnerParam as fn, s3BucketDualStackDomainName as ft, PRE_DELETE_SNAPSHOT_TYPES as g, AssemblyReader as gn, DiffCalculator as gt, ATOMIC_FINAL_SNAPSHOT_TYPES as h, derivePartitionAndUrlSuffix as hn, applyRoleArnIfSet as ht, DeploymentEventsReader as i, resolveUseCdkBootstrapAssets as in, requireConfigArray as it, gray as j, LocalStartServiceError as jn, rewriteTemplateAssetReferences as jt, bold as k, LocalInvokeBuildError as kn, createAssetRedirectResolver as kt, replayRollback as l, MIGRATE_TMP_PREFIX as ln, scrubResourceRecord as lt, computeImplicitDeleteEdges as m, canonicalizeRegion as mn, s3BucketWebsiteUrl as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, resolveStateBucketWithDefault as nn, readConfigString as nt, planFailedOps as o, warnDeprecatedNoPrefixCliFlag as on, requireConfigString as ot, IMPLICIT_DELETE_DEPENDENCIES as p, PARTITION_TABLE as pn, s3BucketRegionalDomainName as pt, getAccountInfo as q, isMarkedNonRetryable as qn, getDockerImageBySourceHash as qt, DeployEngine as r, resolveStateBucketWithDefaultAndSource as rn, replayWarn as rt, planRollback as s, CFN_TEMPLATE_BODY_LIMIT as sn, TEMPLATE_SOURCED_READBACK_RULES as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, resolveSkipPrefix as tn, configStringRefusal as tt, UNSPECIFIED_SKIP_REASON as u, findLargeInlineResources as un, s3BucketArn as ut, ccRoutedFinalSnapshotError as v, clearBucketRegionCache as vn, withRetry as vt, extractDeploymentEventError as w, AssetError as wn, shouldRetainResource as wt, refusesFinalSnapshot as x, getAwsClients as xn, LockManager as xt, createPreDeleteFinalSnapshot as y, resolveBucketRegion as yn, DagBuilder as yt, findActionableSilentDrops as z, StackHasActiveImportsError as zn, validateContainerRepoName as zt };
|
|
24485
|
+
//# sourceMappingURL=deploy-engine-MHqWGHl9.js.map
|