@go-to-k/cdkd 0.283.24 → 0.283.26

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.
@@ -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
@@ -9857,6 +9932,26 @@ const TEMPLATE_SOURCED_READBACK_RULES = {
9857
9932
  descendArrays: false,
9858
9933
  trustAnyExpression: false
9859
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
+ };
9860
9955
  function isDynamicReferenceString(value) {
9861
9956
  return typeof value === "string" && value.includes("{{resolve:");
9862
9957
  }
@@ -9874,14 +9969,16 @@ function isDynamicReferenceString(value) {
9874
9969
  * (issue #1901), which is not derivable from the string, so that arm consults
9875
9970
  * what the resolver actually recorded.
9876
9971
  *
9877
- * The residue is narrow and safe: two SecureString ssm references sharing one
9878
- * resolved value still collapse, because the map cannot distinguish them and
9879
- * spelling cannot either. That is the pre-existing behavior, it leaks nothing
9880
- * (both are redacted, just to one expression), and closing it needs the resolver
9881
- * to record expressions in their own set rather than as map values.
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).
9882
9979
  */
9883
9980
  function isKnownSecretExpression(expression, secretExpressions) {
9884
- return expression.startsWith("{{resolve:secretsmanager:") || secretExpressions.has(expression);
9981
+ return expression.startsWith("{{resolve:secretsmanager:") || secretExpressions.has(expression) || isRecordedSecretExpression(expression);
9885
9982
  }
9886
9983
  /**
9887
9984
  * Is this source leaf a SINGLE complete `{{resolve:...}}` token and nothing
@@ -9901,6 +9998,252 @@ function isPlainObject$2(value) {
9901
9998
  return value !== null && typeof value === "object" && !Array.isArray(value);
9902
9999
  }
9903
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
+ /**
9904
10247
  * PATH-based redaction: walk `bag` alongside a SOURCE bag that still carries the
9905
10248
  * unresolved `{{resolve:...}}` expressions, and wherever the source leaf is such
9906
10249
  * a string, persist THAT string verbatim.
@@ -9919,17 +10262,29 @@ function isPlainObject$2(value) {
9919
10262
  * record — which already holds the expressions — redacts it with no secret
9920
10263
  * fetch and no value matching.
9921
10264
  *
9922
- * The value scan is still applied wherever the source cannot answer: a source
9923
- * leaf that is an intrinsic OBJECT (`Fn::Join` / `Fn::Sub`) resolves to a string
9924
- * embedding the secret, and only a value match can find it inside. So the two
9925
- * passes are complementary rather than alternatives — path where position is
9926
- * knowable, value where it is not.
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.
9927
10277
  */
9928
10278
  function redactByPath(bag, source, secrets, rules, secretExpressions) {
10279
+ if (typeof bag === "string" && isSingleDynamicReferenceToken(bag)) return bag;
9929
10280
  if (isDynamicReferenceString(source) && typeof bag === "string") {
9930
10281
  if (isSingleDynamicReferenceToken(source) && (rules.trustAnyExpression || isKnownSecretExpression(source, secretExpressions))) return source;
9931
10282
  return redactSecretsForState(bag, secrets);
9932
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
+ }
9933
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));
9934
10289
  if (isPlainObject$2(bag) && isPlainObject$2(source)) {
9935
10290
  const out = {};
@@ -11591,28 +11946,51 @@ const cachedAvailabilityZones = {};
11591
11946
  */
11592
11947
  const cachedDynamicReferences = {};
11593
11948
  /**
11594
- * The `{{resolve:ssm:...}}` expressions this process has PROVEN point at a
11595
- * `SecureString` parameter (issue #1901).
11596
- *
11597
- * A plain `ssm` reference is not a secret by SPELLING the way `secretsmanager`
11598
- * is — whether it resolves to public config or to a decrypted secret depends on
11599
- * the parameter's `Type`, which is only knowable from the `GetParameter`
11600
- * response. So secret-ness is discovered on the first resolution and remembered
11601
- * here, keyed by the full `{{resolve:...}}` expression, for the same lifetime as
11602
- * {@link cachedDynamicReferences}.
11603
- *
11604
- * Two consumers need it AFTER the lookup that populated it: the cache-hit arm
11605
- * (which must re-record the value as a secret for the current resolution pass)
11606
- * and the diff / no-op path (which must leave a SecureString reference
11607
- * unresolved without paying a lookup at all once the type is known). A
11608
- * reference NOT in this set is only "not known to be secure" — never "proven
11609
- * public" — so every arm that would leak still asks AWS for the type first.
11610
- *
11611
- * Only the TYPE is remembered, never the decrypted value: on the diff path the
11612
- * lookup is made with `WithDecryption: false`, so the plaintext is never
11613
- * fetched at all there.
11614
- */
11615
- const secureStringSsmReferences = /* @__PURE__ */ new Set();
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
+ };
11616
11994
  /**
11617
11995
  * Cache for EC2 instance attributes that require a live DescribeInstances
11618
11996
  * lookup (PrivateIp / PublicIp / PrivateDnsName / PublicDnsName /
@@ -13424,7 +13802,7 @@ var IntrinsicFunctionResolver = class {
13424
13802
  });
13425
13803
  for (const { fullMatch, inner } of matches) {
13426
13804
  const service = inner.split(":")[0];
13427
- const isKnownSecret = service === "secretsmanager" || secureStringSsmReferences.has(fullMatch);
13805
+ const isKnownSecret = service === "secretsmanager" || recordedSecretExpressions.has(fullMatch);
13428
13806
  if (isKnownSecret && context?.skipDynamicReferences) continue;
13429
13807
  if (fullMatch in cachedDynamicReferences) {
13430
13808
  const cached = cachedDynamicReferences[fullMatch];
@@ -13439,8 +13817,8 @@ var IntrinsicFunctionResolver = class {
13439
13817
  else if (service === "ssm") {
13440
13818
  const decrypt = context?.skipDynamicReferences !== true;
13441
13819
  const param = await this.resolveSSMReference(parts, decrypt);
13442
- if (param.type === "SecureString") secureStringSsmReferences.add(fullMatch);
13443
- else if (!param.secure) secureStringSsmReferences.delete(fullMatch);
13820
+ if (param.type === "SecureString") recordedSecretExpressions.add(fullMatch);
13821
+ else if (!param.secure) recordedSecretExpressions.delete(fullMatch);
13444
13822
  isSecret = param.secure;
13445
13823
  if (param.secure) {
13446
13824
  if (!decrypt) continue;
@@ -13451,7 +13829,10 @@ var IntrinsicFunctionResolver = class {
13451
13829
  continue;
13452
13830
  }
13453
13831
  cachedDynamicReferences[fullMatch] = resolved;
13454
- if (isSecret && resolved) context?.recordedSecretValues?.set(resolved, fullMatch);
13832
+ if (isSecret && resolved) {
13833
+ context?.recordedSecretValues?.set(resolved, fullMatch);
13834
+ if (service === "secretsmanager") recordedSecretExpressions.add(fullMatch);
13835
+ }
13455
13836
  result = result.replace(fullMatch, () => resolved);
13456
13837
  }
13457
13838
  return result;
@@ -14506,7 +14887,7 @@ var CloudControlProvider = class {
14506
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);
14507
14888
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
14508
14889
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
14509
- const { ASGProvider } = await import("./asg-provider-C9Mq493c.js").then((n) => n.n);
14890
+ const { ASGProvider } = await import("./asg-provider-CshsD-Pi.js").then((n) => n.n);
14510
14891
  return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
14511
14892
  }
14512
14893
  const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
@@ -21127,10 +21508,35 @@ async function resolveReplayProps(props, resolver, secrets) {
21127
21508
  * (GHSA fix). The record's `properties` may be the provider's
21128
21509
  * `effectiveProperties`, which can echo the value we just resolved for the
21129
21510
  * provider call — so scrub it with the same per-op secrets map before it is
21130
- * persisted. No-op when the op resolved no secret.
21131
- */
21132
- function redactRollbackRecord(record, secrets) {
21133
- return secrets.size > 0 ? scrubResourceRecord(record, secrets) : record;
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);
21134
21540
  }
21135
21541
  async function updateWithRollbackRetry(provider, args, logicalId, logger, isInterrupted) {
21136
21542
  if (provider.disableOuterRetry) return await provider.update(...args);
@@ -21376,7 +21782,7 @@ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds
21376
21782
  physicalId: createResult.physicalId,
21377
21783
  attributes: createResult.attributes ?? {},
21378
21784
  properties: recordedPropertiesAfterReplayCreate(prevRecord, createResult)
21379
- }, secrets);
21785
+ }, secrets, prevRecord.properties);
21380
21786
  await afterOp?.(op.logicalId);
21381
21787
  if (!deletedNewFirst && !adoptedLiveNewResource) try {
21382
21788
  const finalSnapshotIdentifier = rollbackFinalSnapshotId(op.resourceType, current, op.provisionedBy);
@@ -21427,7 +21833,7 @@ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds
21427
21833
  desiredProps ?? {},
21428
21834
  currentProps ?? {}
21429
21835
  ], op.logicalId, logger, isInterrupted);
21430
- stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(previousState, revertResult), secrets);
21836
+ stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(previousState, revertResult), secrets, previousState.properties);
21431
21837
  logger.info(` Rollback: ${op.logicalId} restored successfully`);
21432
21838
  await afterOp?.(op.logicalId);
21433
21839
  ctx.recordEvent?.({
@@ -21567,7 +21973,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
21567
21973
  desiredProps ?? {},
21568
21974
  attemptedProps ?? {}
21569
21975
  ], op.logicalId, logger, options.isInterrupted);
21570
- stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(prev, revertFailedResult), secrets);
21976
+ stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(prev, revertFailedResult), secrets, prev.properties);
21571
21977
  logger.info(` Rollback: ${op.logicalId} reverted successfully`);
21572
21978
  await options.afterOp?.(op.logicalId);
21573
21979
  ctx.recordEvent?.({
@@ -21693,7 +22099,7 @@ const FLUSH_INTERVAL_MS = 2e3;
21693
22099
  const FLUSH_EVENT_THRESHOLD = 50;
21694
22100
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
21695
22101
  function getCdkdVersion() {
21696
- return "0.283.24";
22102
+ return "0.283.26";
21697
22103
  }
21698
22104
  /**
21699
22105
  * Generate a time-sortable unique run id, e.g.
@@ -22325,6 +22731,15 @@ var DeployEngine = class {
22325
22731
  */
22326
22732
  outputSecrets = /* @__PURE__ */ new Map();
22327
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
+ /**
22328
22743
  * Per-logical-id snapshot of the intrinsic-RESOLVED desired properties
22329
22744
  * each CREATE / UPDATE attempted (issue #1198). Written just before the
22330
22745
  * provider call; read only when the op FAILS, to journal the failed op's
@@ -22368,6 +22783,7 @@ var DeployEngine = class {
22368
22783
  this.perResourceSecrets = /* @__PURE__ */ new Map();
22369
22784
  this.perResourceTemplateProps = /* @__PURE__ */ new Map();
22370
22785
  this.outputSecrets = /* @__PURE__ */ new Map();
22786
+ this.outputsTemplateSource = {};
22371
22787
  this.resolver.resetPhysicalIdFallbackCount();
22372
22788
  return withStackName(stackName, () => this.doDeploy(stackName, template));
22373
22789
  }
@@ -22399,13 +22815,19 @@ var DeployEngine = class {
22399
22815
  * un-redacted resolved bag; only the persisted copy is rewritten.
22400
22816
  */
22401
22817
  /**
22402
- * Bag-level redaction of a single properties bag against a specific secrets
22403
- * map (GHSA fix). Used by the UPDATE no-op re-check so a resolved-plaintext
22404
- * bag is compared against the expression-bearing stored bag on equal footing.
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.
22405
22827
  */
22406
- redactPropsWith(props, secrets) {
22407
- if (secrets.size === 0) return props;
22408
- return redactSecretsForState(props, secrets);
22828
+ redactOutputs(outputs) {
22829
+ if (this.outputSecrets.size === 0) return outputs;
22830
+ return redactSecretsForState(outputs, this.outputSecrets, this.outputsTemplateSource);
22409
22831
  }
22410
22832
  /**
22411
22833
  * Redact resolved secret plaintext out of rollback-journal operations (GHSA
@@ -22416,21 +22838,34 @@ var DeployEngine = class {
22416
22838
  * whole-secret value from one resource cannot rewrite another's literal.
22417
22839
  * Preserves ops that carry none of those fields, or whose resource recorded
22418
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.
22419
22858
  */
22420
22859
  redactOperationsForJournal(operations) {
22421
22860
  return operations.map((op) => {
22422
22861
  const secrets = this.perResourceSecrets.get(op.logicalId);
22423
- if (!secrets || secrets.size === 0) return op;
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();
22424
22865
  const next = { ...op };
22425
- if (next.properties) next.properties = redactSecretsForState(next.properties, secrets);
22426
- if (next.attemptedProperties) next.attemptedProperties = redactSecretsForState(next.attemptedProperties, secrets);
22427
- if (next.previousState) {
22428
- const prev = { ...next.previousState };
22429
- prev.properties = redactSecretsForState(prev.properties, secrets);
22430
- if (prev.attributes) prev.attributes = redactSecretsForState(prev.attributes, secrets);
22431
- if (prev.observedProperties) prev.observedProperties = redactSecretsForState(prev.observedProperties, secrets);
22432
- next.previousState = prev;
22433
- }
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);
22434
22869
  return next;
22435
22870
  });
22436
22871
  }
@@ -22444,7 +22879,7 @@ var DeployEngine = class {
22444
22879
  return {
22445
22880
  ...state,
22446
22881
  resources,
22447
- outputs: this.outputSecrets.size > 0 ? redactSecretsForState(state.outputs, this.outputSecrets) : state.outputs
22882
+ outputs: this.redactOutputs(state.outputs)
22448
22883
  };
22449
22884
  }
22450
22885
  /**
@@ -22729,7 +23164,7 @@ var DeployEngine = class {
22729
23164
  this.logger.info("No changes detected. Stack is up to date.");
22730
23165
  let persistedOutputs = currentState.outputs ?? {};
22731
23166
  if (!this.options.dryRun) {
22732
- const resolvedOutputs = this.redactPropsWith(await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, parameterValues, conditions), this.outputSecrets);
23167
+ const resolvedOutputs = this.redactOutputs(await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, parameterValues, conditions));
22733
23168
  const resolutionFailed = Object.values(resolvedOutputs).some((v) => v === void 0);
22734
23169
  const outputsChanged = !resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs);
22735
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.");
@@ -23057,7 +23492,7 @@ var DeployEngine = class {
23057
23492
  let outputs;
23058
23493
  try {
23059
23494
  outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
23060
- outputs = this.redactPropsWith(outputs, this.outputSecrets);
23495
+ outputs = this.redactOutputs(outputs);
23061
23496
  } catch (outputError) {
23062
23497
  await this.persistStateAfterOutputFailure(stackName, currentState, newResources, currentEtag, pendingMigration);
23063
23498
  await this.writeRollbackJournalSegment(stackName, completedOperations, failedOperations, "no-rollback-failure", currentEtag === void 0);
@@ -23501,7 +23936,7 @@ var DeployEngine = class {
23501
23936
  this.perResourceTemplateProps.set(logicalId, desiredProps);
23502
23937
  this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
23503
23938
  this.attemptedResolvedProps.set(logicalId, resolvedProps);
23504
- if (JSON.stringify(this.redactPropsWith(resolvedProps, updateSecrets)) === JSON.stringify(currentProps)) {
23939
+ if (JSON.stringify(redactSecretsForState(resolvedProps, updateSecrets, desiredProps)) === JSON.stringify(currentProps)) {
23505
23940
  if (change.attributeChanges && change.attributeChanges.length > 0) {
23506
23941
  const attrSummary = change.attributeChanges.map((a) => `${a.attribute}: ${a.oldValue ?? "(unset)"} → ${a.newValue ?? "(unset)"}`).join(", ");
23507
23942
  this.logger.info(` ↻ ${logicalId} (${resourceType}) attribute update: ${attrSummary}`);
@@ -24019,7 +24454,10 @@ var DeployEngine = class {
24019
24454
  outputs[outputKey] = value;
24020
24455
  if (output.Export?.Name) {
24021
24456
  const exportName = typeof output.Export.Name === "string" ? output.Export.Name : await this.resolver.resolve(output.Export.Name, context);
24022
- if (typeof exportName === "string") outputs[exportName] = value;
24457
+ if (typeof exportName === "string") {
24458
+ outputs[exportName] = value;
24459
+ this.outputsTemplateSource[exportName] = output.Value;
24460
+ }
24023
24461
  }
24024
24462
  } catch (error) {
24025
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 });
@@ -24028,6 +24466,7 @@ var DeployEngine = class {
24028
24466
  }
24029
24467
  }
24030
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;
24031
24470
  return outputs;
24032
24471
  }
24033
24472
  buildDisplayOutputs(template, resolvedOutputs) {
@@ -24042,5 +24481,5 @@ var DeployEngine = class {
24042
24481
  };
24043
24482
 
24044
24483
  //#endregion
24045
- export { coerceCfnBoolean as $, resolveCaptureObservedState as $t, cyan as A, LocalStartServiceError as An, rewriteTemplateAssetReferences as At, findSilentDropProperties as B, StateError as Bn, buildDockerImage as Bt, makeCanonicalizePropertiesFn as C, AssetError as Cn, shouldRetainResource as Ct, renderStatefulReason as D, DeployCancelledError as Dn, buildAssetRedirectMap as Dt, isStatefulRecreateTargetSync as E, DependencyError as En, WorkGraph as Et, IAMRoleProvider as F, ProvisioningError as Fn, getBootstrapMarkerKey as Ft, IntrinsicFunctionResolver as G, withErrorHandling as Gn, AssetManifestLoader as Gt, slowCcOperationTimeoutMs as H, formatError as Hn, getDockerCmd as Ht, collectInlinePolicyNamesManagedBySiblings as I, ResourceTimeoutError as In, parseBootstrapMarker as It, refStateLookupFromResource as J, isThrottlingError as Jn, synthesisStatusMessage as Jt, cfnRefValueFromPhysicalId as K, isMarkedNonRetryable as Kn, getDockerImageBySourceHash as Kt, clearOnUpdateRemoval as L, ResourceUpdateNotSupportedError as Ln, validateAssetBucketName as Lt, green as M, MissingCdkCliError as Mn, AssetModeResolver as Mt, red as N, NestedStackChildDirectDestroyError as Nn, BOOTSTRAP_MARKER_PREFIX as Nt, formatResourceLine as O, LocalInvokeBuildError as On, createAssetRedirectResolver as Ot, yellow as P, PartialFailureError as Pn, ensureAssetStorage as Pt, assertRegionMatch as Q, resolveAutoAssetStorage as Qt, ProviderRegistry as R, StackHasActiveImportsError as Rn, validateContainerRepoName as Rt, unsupportedFinalSnapshotError as S, setAwsClients as Sn, rebuildClientForBucketRegion as St, MULTI_REGION_RECREATE_BLOCKED_TYPES as T, ConfigError as Tn, stringifyValue as Tt, disableInstanceApiTermination as U, isCdkdError as Un, runDockerForeground as Ut, CloudControlProvider as V, SynthesisError as Vn, formatDockerLoginError as Vt, isTerminationProtectionPropagationError as W, normalizeAwsError as Wn, runDockerStreaming as Wt, normalizeAwsTagsToCfn as X, __exportAll as Xn, getLegacyStateBucketName as Xt, WAFv2WebACLProvider as Y, markNonRetryable as Yn, getDefaultStateBucketName as Yt, resolveExplicitPhysicalId as Z, resolveApp as Zt, buildFinalSnapshotIdentifier as _, clearBucketRegionCache as _n, withRetry as _t, DeploymentEventsStore as a, warnDeprecatedNoPrefixCliFlag as an, requireConfigObject as at, isFinalSnapshotError as b, getAwsClients as bn, LockManager as bt, replayFailedOperations as c, MIGRATE_TMP_PREFIX as cn, scrubResourceRecord as ct, deleteSkipReason as d, expectedOwnerParam as dn, s3BucketDualStackDomainName as dt, resolveSkipPrefix as en, configBooleanRefusal as et, withResourceDeadline as f, PARTITION_TABLE as fn, s3BucketRegionalDomainName as ft, PRE_DELETE_SNAPSHOT_TYPES as g, processStackMessages as gn, describeTypeWithThrottleRetry as gt, ATOMIC_FINAL_SNAPSHOT_TYPES as h, AssemblyReader as hn, DiffCalculator as ht, DeploymentEventsReader as i, stateBucketExistenceConfirmed as in, requireConfigArray as it, gray as j, LockError as jn, escapeRegExp$1 as jt, bold as k, LocalMigrateError as kn, loadPublishableAssetManifest as kt, replayRollback as l, findLargeInlineResources as ln, s3BucketArn as lt, computeImplicitDeleteEdges as m, derivePartitionAndUrlSuffix as mn, applyRoleArnIfSet as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, resolveStateBucketWithDefaultAndSource as nn, readConfigString as nt, planFailedOps as o, CFN_TEMPLATE_BODY_LIMIT as on, requireConfigString as ot, IMPLICIT_DELETE_DEPENDENCIES as p, canonicalizeRegion as pn, s3BucketWebsiteUrl as pt, getAccountInfo as q, isRetryableTransientError as qn, Synthesizer as qt, DeployEngine as r, resolveUseCdkBootstrapAssets as rn, replayWarn as rt, planRollback as s, CFN_TEMPLATE_URL_LIMIT as sn, redactSecretsForState as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, resolveStateBucketWithDefault as tn, configStringRefusal as tt, UNSPECIFIED_SKIP_REASON as u, uploadCfnTemplate as un, s3BucketDomainName as ut, ccRoutedFinalSnapshotError as v, resolveBucketRegion as vn, DagBuilder as vt, extractDeploymentEventError as w, CdkdError as wn, AssetPublisher as wt, refusesFinalSnapshot as x, resetAwsClients as xn, S3StateBackend as xt, createPreDeleteFinalSnapshot as y, AwsClients as yn, TemplateParser as yt, findActionableSilentDrops as z, StackTerminationProtectionError as zn, buildDenyExternalAccessPolicy as zt };
24046
- //# sourceMappingURL=deploy-engine-Cu6OO-8t.js.map
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-B6z9CCAL.js.map