@go-to-k/cdkd 0.285.8 → 0.285.9

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.
@@ -1,6 +1,6 @@
1
1
  import { _ as withStackName, d as applyDefaultNameForFallback, f as generateResourceName, h as looksLikeCdkdGeneratedName, m as getCurrentStackName, n as getLogger, p as generateResourceNameWithFallback, r as isStdoutReservedForPayload, s as getLiveRenderer } from "./logger-Dw-3y48G.js";
2
2
  import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
3
- import { t as getCdkdVersion } from "./version-DFdq83MF.js";
3
+ import { t as getCdkdVersion } from "./version-BWsyP2sq.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -9638,6 +9638,297 @@ function isRecordedSecretExpression(expression) {
9638
9638
  function clearRecordedSecretExpressions() {
9639
9639
  recordedSecretExpressions$1.clear();
9640
9640
  }
9641
+ /**
9642
+ * The plaintexts a pass recorded with NO EXPRESSION behind them — the
9643
+ * MASK-ONLY needle class (issue
9644
+ * [#2274](https://github.com/go-to-k/cdkd/issues/2274)).
9645
+ *
9646
+ * WHAT IT IS FOR. A Lambda-backed custom resource's handler can declare its
9647
+ * response `Data` sensitive with the documented `NoEcho: true` envelope field.
9648
+ * That value is GENERATED by the handler, so cdkd never substituted it from
9649
+ * anything: there is no `{{resolve:...}}` expression to rewrite it back onto,
9650
+ * which is exactly why {@link RecordedSecretValues} — a plaintext -> EXPRESSION
9651
+ * map — cannot hold it on its own terms. The value still must not sit in
9652
+ * `state.json`, so what gets persisted in its place is {@link SECRET_MASK}.
9653
+ *
9654
+ * WHY THE SAME MAP RATHER THAN A SECOND BAG. Every persistence reader in this
9655
+ * module already walks a `RecordedSecretValues` — `scrubResourceRecord`'s three
9656
+ * fields, the rollback journal's ops, the outputs bag, `maskSecretsInText`'s
9657
+ * log / error / event sites. Threading a parallel bag to each of them would be
9658
+ * a wide change with one place per reader to forget. Recording the pair as
9659
+ * `plaintext -> SECRET_MASK` means the whole-value arm of
9660
+ * {@link redactSecretsForState} substitutes the mask with no code change at
9661
+ * all, and every reader is covered by construction.
9662
+ *
9663
+ * THE SENTINEL VALUE **IS** THE MARKER — there is no side table, and an earlier
9664
+ * revision's `WeakMap<RecordedSecretValues, Set<string>>` was removed after a
9665
+ * mutation probe showed the extra conjunct could not be fenced AND pointed the
9666
+ * wrong way. Nothing but {@link recordMaskOnlyValue} ever writes
9667
+ * {@link SECRET_MASK} as a map VALUE (every other writer stores a whole
9668
+ * `{{resolve:...}}` token, and {@link recordCrossStackExpression} refuses
9669
+ * anything else), so the side table could only ever disagree about an entry
9670
+ * some future writer valued `***` by hand — and for THAT entry, withholding the
9671
+ * substring arm is the SAFE answer, which is what the side table would have
9672
+ * denied. Scope is unaffected: the MAP is already per-pass, so a mask cannot
9673
+ * reach another resource's bag any more than an expression can.
9674
+ *
9675
+ * THE ONE PLACE THE CLASSES MUST DIFFER: the SUBSTRING arm. Substituting an
9676
+ * EXPRESSION for a match inside a longer leaf is lossless — the persisted leaf
9677
+ * still names a value every downstream reader can re-resolve. Substituting a
9678
+ * MASK is not: an inline `***` is indistinguishable from a literal `***` a user
9679
+ * wrote, so no consumer can recognise it, and `cdkd drift --revert` /
9680
+ * `resolveReplayProps` would push the corrupted string to AWS. A mask is only
9681
+ * safe where it is RECOGNISABLE, and that means whole-leaf. So a mask-only
9682
+ * plaintext is excluded from the persist path's needle regex and reaches only
9683
+ * the whole-value arm — the same "weaker class, narrower blast radius" shape
9684
+ * PR #2415 established for its `inferred` needles, which likewise take a leaf
9685
+ * whole or not at all.
9686
+ *
9687
+ * {@link maskSecretsInText} is deliberately NOT narrowed the same way: its
9688
+ * output is a log line, an error message or an event, which nothing reads back
9689
+ * as a value, so a partial mask there costs nothing and closes an embedded
9690
+ * disclosure.
9691
+ */
9692
+ /**
9693
+ * Record `plaintext` as MASK-ONLY in `secrets` — persist {@link SECRET_MASK} in
9694
+ * its place, with no expression to substitute (issue #2274).
9695
+ *
9696
+ * An EXPRESSION already recorded for the same plaintext WINS and this is a
9697
+ * no-op: an expression is strictly better than a mask (it is re-resolvable, it
9698
+ * survives `drift --revert` and the rollback replay, and it reaches the
9699
+ * substring arm), so a mask must never demote one. The reverse direction needs
9700
+ * no code: the resolver writes an expression straight into the map, and
9701
+ * {@link isMaskOnlyPlaintext} re-checks the map value, so a plaintext that
9702
+ * later acquires a real expression stops being mask-only immediately.
9703
+ *
9704
+ * A plaintext shorter than {@link MIN_NEEDLE_LENGTH} is REFUSED, and this floor
9705
+ * is the one place the mask class needs a bound the EXPRESSION class does not
9706
+ * (issue #2274 review). An expression-bearing needle below the threshold is
9707
+ * still substituted on the WHOLE-VALUE arm, and that is safe because the pair
9708
+ * came from a POSITION cdkd resolved: the leaf it rewrites provably held that
9709
+ * reference. A mask-only needle has no position behind it — it is a bare
9710
+ * plaintext the handler happened to return — so the whole-value arm masks EVERY
9711
+ * leaf equal to it, anywhere in the record. A handler answering
9712
+ * `Data: { Count: "7" }` would otherwise mask any property whose whole value is
9713
+ * `"7"`, unrecoverably (there is no expression to re-resolve) and on every
9714
+ * later run (the mask then trips `refuseRedactedAttributeReads`,
9715
+ * `refuseMaskedReplayBaseline` and the export blocker). The floor is the same
9716
+ * constant the substring arm already applies, so the two arms of this module
9717
+ * now agree about what is too short to be a distinguishing value.
9718
+ *
9719
+ * THE BOUND IS THE MODULE'S, NOT ONE THIS CHANNEL INVENTED, and it is stated
9720
+ * rather than overstated: {@link MIN_NEEDLE_LENGTH} is 4, so a FOUR-character
9721
+ * member (`"true"`) still becomes a needle and a property whose whole value is
9722
+ * `"true"` is still masked. Raising the floor here alone would fork the two
9723
+ * arms' idea of a distinguishing value, which is the disagreement the shared
9724
+ * constant exists to prevent. The remedy for that shape is a handler contract
9725
+ * — do not declare a whole response `NoEcho` when its `Data` mixes a secret
9726
+ * with short non-secret members — and it is asserted in
9727
+ * `secret-redaction-mask-only.test.ts` so the bound is a recorded decision
9728
+ * rather than a surprise.
9729
+ *
9730
+ * The empty string is refused by the same bound, and would be refused anyway
9731
+ * for the reason the value pass refuses it: it is not a distinguishing value,
9732
+ * and recording it would mask every empty leaf.
9733
+ */
9734
+ function recordMaskOnlyValue(secrets, plaintext) {
9735
+ if (plaintext.length < 4) return;
9736
+ const existing = secrets.get(plaintext);
9737
+ if (existing !== void 0 && existing !== "***") return;
9738
+ secrets.set(plaintext, "***");
9739
+ }
9740
+ /**
9741
+ * Is `plaintext` a MASK-ONLY entry of `secrets`?
9742
+ *
9743
+ * Read off the MAP, so a plaintext this module marked as mask-only and the
9744
+ * resolver later records WITH an expression stops being one immediately —
9745
+ * which matters, because that entry has earned the substring arm back.
9746
+ */
9747
+ function isMaskOnlyPlaintext(secrets, plaintext) {
9748
+ return secrets.get(plaintext) === "***";
9749
+ }
9750
+ /**
9751
+ * Record every STRING LEAF of `value` as a MASK-ONLY needle in `secrets`.
9752
+ *
9753
+ * The bag-shaped twin of {@link recordMaskOnlyValue}, used where a whole
9754
+ * `Data` / attributes object is declared sensitive at once. Non-string leaves
9755
+ * are skipped deliberately: the redaction walk matches by string value, so
9756
+ * there is nothing to key a number or a boolean on, and both are far too
9757
+ * collision-prone to be useful needles even if there were.
9758
+ *
9759
+ * `excluded` is the set of plaintexts CDKD ITSELF SUPPLIED to the resource, and
9760
+ * passing it is what keeps a handler from masking cdkd's own inputs back at it
9761
+ * (issue #2274 review). A handler echoing its `event.ResourceProperties` into
9762
+ * `Data` — the shape the CDK `Provider` framework's samples encourage — makes
9763
+ * `Data.X` equal to the resource's own `ServiceToken`, and recording THAT as a
9764
+ * needle rewrites `properties.ServiceToken` to `***` in the very record
9765
+ * `CustomResourceProvider.delete` reads it back from, where `'***'` is a
9766
+ * truthy string that passes both of that method's guards. Such a value is not
9767
+ * handler-GENERATED at all — it is in the synthesized template already — so
9768
+ * excluding it costs no secrecy.
9769
+ */
9770
+ function recordMaskOnlyValuesIn(value, secrets, excluded) {
9771
+ const seen = /* @__PURE__ */ new Set();
9772
+ const walk = (node) => {
9773
+ if (typeof node === "string") {
9774
+ if (excluded?.has(node) === true) return;
9775
+ recordMaskOnlyValue(secrets, node);
9776
+ return;
9777
+ }
9778
+ if (node === null || typeof node !== "object") return;
9779
+ if (seen.has(node)) return;
9780
+ seen.add(node);
9781
+ if (Array.isArray(node)) {
9782
+ for (const item of node) walk(item);
9783
+ return;
9784
+ }
9785
+ for (const child of Object.values(node)) walk(child);
9786
+ };
9787
+ walk(value);
9788
+ }
9789
+ /**
9790
+ * Every WHOLE string leaf of `value`, as a set — the `excluded` argument
9791
+ * {@link recordMaskOnlyValuesIn} takes, built from the resource's own resolved
9792
+ * template properties.
9793
+ *
9794
+ * WHOLE leaves only, matching the arm the mask class is served on: a mask-only
9795
+ * needle never reaches the substring scan, so a plaintext that merely OCCURS
9796
+ * inside a property is not something this exclusion has to answer for.
9797
+ */
9798
+ function wholeStringLeavesOf(value) {
9799
+ const leaves = /* @__PURE__ */ new Set();
9800
+ const seen = /* @__PURE__ */ new Set();
9801
+ const walk = (node) => {
9802
+ if (typeof node === "string") {
9803
+ leaves.add(node);
9804
+ return;
9805
+ }
9806
+ if (node === null || typeof node !== "object") return;
9807
+ if (seen.has(node)) return;
9808
+ seen.add(node);
9809
+ if (Array.isArray(node)) {
9810
+ for (const item of node) walk(item);
9811
+ return;
9812
+ }
9813
+ for (const child of Object.values(node)) walk(child);
9814
+ };
9815
+ walk(value);
9816
+ return leaves;
9817
+ }
9818
+ /**
9819
+ * Does `value` carry {@link SECRET_MASK} as a WHOLE string leaf?
9820
+ *
9821
+ * The recognition test every consumer of a REDACTED baseline shares (issue
9822
+ * #2274). A mask-only redaction is whole-leaf precisely so it stays
9823
+ * recognisable, and this is what recognises it — in the resolver (a persisted
9824
+ * attribute cdkd can no longer serve), in `cdkd drift` (a baseline that must
9825
+ * not be pushed by `--revert` nor overwritten by `--accept`), and in the
9826
+ * rollback replay (a desired bag that must not reach a provider).
9827
+ *
9828
+ * Whole-leaf EQUALITY, never containment: an inline `***` inside a longer
9829
+ * string is either a user's own literal or text this module never wrote, and
9830
+ * treating it as a mask would refuse ordinary values. The corresponding limit —
9831
+ * a NoEcho value EMBEDDED in a larger leaf keeps its plaintext — is the same
9832
+ * one the mask-only channel note above states, and is tracked separately.
9833
+ *
9834
+ * UNBOUNDED in depth, guarded by {@link WalkedContainers} — see that type for
9835
+ * why a depth cap here was a hole rather than a safety measure.
9836
+ */
9837
+ function carriesSecretMask(value) {
9838
+ const seen = /* @__PURE__ */ new Set();
9839
+ const walk = (node) => {
9840
+ if (typeof node === "string") return node === "***";
9841
+ if (node === null || typeof node !== "object") return false;
9842
+ if (seen.has(node)) return false;
9843
+ seen.add(node);
9844
+ if (Array.isArray(node)) return node.some((item) => walk(item));
9845
+ return Object.values(node).some((child) => walk(child));
9846
+ };
9847
+ return walk(value);
9848
+ }
9849
+ /**
9850
+ * The plaintexts the PERSIST path may scan for as SUBSTRINGS — every recorded
9851
+ * one except the mask-only class. See the mask-only channel note above for why the
9852
+ * mask class is whole-leaf only.
9853
+ */
9854
+ function substringNeedlesOf(secrets) {
9855
+ const needles = [];
9856
+ for (const plaintext of secrets.keys()) if (!isMaskOnlyPlaintext(secrets, plaintext)) needles.push(plaintext);
9857
+ return needles;
9858
+ }
9859
+ /**
9860
+ * The EXPRESSIONS a pass recorded — `secrets.values()` minus the mask-only
9861
+ * class, whose "expression" is the mask sentinel rather than a reference.
9862
+ *
9863
+ * Removing this filter is an EQUIVALENT MUTANT and no test can red on it —
9864
+ * stated rather than claimed pinned. {@link SECRET_MASK} is not a
9865
+ * dynamic-reference token, so neither `isKnownSecretExpression` nor a skeleton
9866
+ * pattern can ever accept it as a candidate. It is kept because a list
9867
+ * documented as "the expressions this pass recorded" must not silently contain
9868
+ * something that is not one: the day a candidate test stops requiring token
9869
+ * SHAPE, the sentinel would be live in it.
9870
+ */
9871
+ function recordedExpressionsOf(secrets) {
9872
+ const expressions = /* @__PURE__ */ new Set();
9873
+ for (const [plaintext, expression] of secrets) if (!isMaskOnlyPlaintext(secrets, plaintext)) expressions.add(expression);
9874
+ return expressions;
9875
+ }
9876
+ /**
9877
+ * The IN-RUN recovery channel for a stack OUTPUT this process masked (issue
9878
+ * [#2274](https://github.com/go-to-k/cdkd/issues/2274)).
9879
+ *
9880
+ * WHY IT EXISTS. Masking a `NoEcho` custom resource's `Data` on the way into
9881
+ * `state.json` is right within one stack, where `Fn::GetAtt` reads the value out
9882
+ * of the IN-MEMORY record and gets the plaintext. It breaks the moment the value
9883
+ * crosses a STACK boundary, because every cross-stack route reads the producer's
9884
+ * PERSISTED `state.outputs`: a nested stack's `Outputs.<Key>` (via
9885
+ * `NestedStackProvider.readChildOutputsAsAttributes`), `Fn::ImportValue` (via
9886
+ * the exports index or a state scan) and `Fn::GetStackOutput` all land on the
9887
+ * mask. Without this the FIRST deploy of a parent whose child exports such a
9888
+ * value would refuse — a template that deployed before this feature — which is
9889
+ * a regression rather than a trade.
9890
+ *
9891
+ * WHAT IT IS. `stack + region + output key -> the plaintext that key held
9892
+ * before redaction`, written at the moment the producer's outputs are redacted
9893
+ * and read at the three cross-stack sites above. It answers only for a producer
9894
+ * THIS PROCESS deployed in THIS run, which is exactly the population that has a
9895
+ * plaintext to hand back: a separate `cdkd deploy` of the consumer has none, and
9896
+ * that case is refused rather than guessed at.
9897
+ *
9898
+ * WHY THE COORDINATE, and not a plaintext-keyed set. A bare-plaintext store was
9899
+ * the shape PR #2415 was forced to WITHDRAW (`provenPublicExpressions`,
9900
+ * residual #2425): keyed on a value alone, one stack's answer is served to
9901
+ * another stack's identically-spelled read. Here the key names the producer
9902
+ * stack, its region and the output — so a hit is served only to a resolution
9903
+ * that asked for that exact output of that exact stack, i.e. to precisely the
9904
+ * reader that would have received the plaintext before this feature existed.
9905
+ * Nothing is widened.
9906
+ *
9907
+ * A RECOVERED VALUE IS STILL SECRET, and every reader re-registers it as a
9908
+ * mask-only needle in its OWN bag before using it — the recovery hands back the
9909
+ * value for the WIRE, never for persistence.
9910
+ */
9911
+ const recoverableMaskedOutputs = /* @__PURE__ */ new Map();
9912
+ function maskedOutputKey(stackName, region, outputKey) {
9913
+ return `${stackName}\u0000${region}\u0000${outputKey}`;
9914
+ }
9915
+ /**
9916
+ * Remember the plaintext an output held before {@link SECRET_MASK} replaced it.
9917
+ * See {@link recoverableMaskedOutputs}.
9918
+ */
9919
+ function recordRecoverableMaskedOutput(stackName, region, outputKey, plaintext) {
9920
+ recoverableMaskedOutputs.set(maskedOutputKey(stackName, region, outputKey), plaintext);
9921
+ }
9922
+ /**
9923
+ * The plaintext this process masked out of `stackName`'s `outputKey`, or
9924
+ * `undefined` when this run did not produce that output.
9925
+ *
9926
+ * `undefined` is the honest answer for a producer deployed by an EARLIER run:
9927
+ * the value is gone and cdkd must refuse rather than write the mask to AWS.
9928
+ */
9929
+ function recoverMaskedOutput(stackName, region, outputKey) {
9930
+ return recoverableMaskedOutputs.get(maskedOutputKey(stackName, region, outputKey));
9931
+ }
9641
9932
  const crossStackAssociations = /* @__PURE__ */ new WeakMap();
9642
9933
  /** Poison for a key seen against two different (expression, plaintext) pairs. */
9643
9934
  const CONFLICTING_CROSS_STACK = Symbol("conflicting cross-stack association");
@@ -12101,7 +12392,7 @@ function refuseUncertifiedReadbackPositions(bag, source, secrets, learn, mark) {
12101
12392
  function redactSecretsForState(bag, secrets, source, rules = TEMPLATE_DERIVED_RULES) {
12102
12393
  if (secrets.size === 0 && source === void 0) return bag;
12103
12394
  if (source !== void 0) {
12104
- const positioned = redactByPath(bag, source, secrets, rules, new Set(secrets.values()));
12395
+ const positioned = redactByPath(bag, source, secrets, rules, recordedExpressionsOf(secrets));
12105
12396
  if (!isReadbackProjectedFromState(rules)) return positioned;
12106
12397
  const refused = refuseUncertifiedReadbackPositions(positioned, source, secrets);
12107
12398
  const derived = deriveReadbackNeedles(bag, source, secrets, rules);
@@ -12109,7 +12400,7 @@ function redactSecretsForState(bag, secrets, source, rules = TEMPLATE_DERIVED_RU
12109
12400
  const marks = refuseUncertifiedReadbackPositions(positioned, source, secrets, void 0, true);
12110
12401
  return preferPositionDecisions(redactSecretsForState(bag, derived.certain), refused, bag, marks, derived.inferred);
12111
12402
  }
12112
- const regex = buildNeedleRegex(secrets.keys());
12403
+ const regex = buildNeedleRegex(substringNeedlesOf(secrets));
12113
12404
  const wholeValueExpr = (s) => s === "" ? void 0 : secrets.get(s);
12114
12405
  /**
12115
12406
  * The SUBSTRING arm for a leaf the resolver substituted INTO rather than
@@ -12248,6 +12539,15 @@ function scrubResourceRecord(record, secrets, sourceProperties, observedRules) {
12248
12539
  * Used on log lines and error messages where a resolved secret could otherwise
12249
12540
  * be echoed. Whole-value and embedded matches are both masked. Returns `text`
12250
12541
  * unchanged when there is nothing to mask.
12542
+ *
12543
+ * The MASK-ONLY class (issue #2274) participates here FULLY — `secrets.keys()`,
12544
+ * not the persist path's narrowed `substringNeedlesOf` — and the asymmetry is
12545
+ * deliberate rather than an oversight. The persist path withholds the substring
12546
+ * arm from a mask because an inline `***` is a value nothing downstream can
12547
+ * recognise or re-resolve; this output is a log line, an error message or an
12548
+ * event, which no consumer reads back as a value, so a partial mask costs
12549
+ * nothing and closes an EMBEDDED disclosure that would otherwise print. See
12550
+ * the mask-only channel note above.
12251
12551
  */
12252
12552
  function maskSecretsInText(text, secrets) {
12253
12553
  if (secrets.size === 0) return text;
@@ -18384,11 +18684,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
18384
18684
  if (resource.resourceType === "AWS::Route53::HostedZone" && attributeName === "NameServers" && typeof flatValue === "string") {
18385
18685
  const nameServers = flatValue === "" ? [] : flatValue.split(",");
18386
18686
  this.logger.debug(`Normalized legacy Fn::GetAtt attribute: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, nameServers)}`);
18387
- return nameServers;
18687
+ return this.noteAttributeSecrecy(logicalId, attributeName, nameServers, context);
18388
18688
  }
18389
18689
  this.logger.debug(`Resolved Fn::GetAtt from attributes: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, flatValue)}`);
18390
18690
  if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE && attributeName.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX) && carriesDynamicReference(flatValue)) return await this.reresolveCrossStackValue(flatValue, nestedStackChildRegionFromLocalArn(resource.physicalId), context, `nested stack ${logicalId} ${attributeName}`, crossStackSourceKey({ "Fn::GetAtt": getAtt }));
18391
- return flatValue;
18691
+ return this.noteAttributeSecrecy(logicalId, attributeName, flatValue, context);
18392
18692
  }
18393
18693
  if (attributeName.includes(".")) {
18394
18694
  const parts = attributeName.split(".");
@@ -18400,7 +18700,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
18400
18700
  }
18401
18701
  if (cursor !== void 0) {
18402
18702
  this.logger.debug(`Resolved Fn::GetAtt from nested attributes: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, cursor)}`);
18403
- return cursor;
18703
+ return this.noteAttributeSecrecy(logicalId, attributeName, cursor, context);
18404
18704
  }
18405
18705
  }
18406
18706
  }
@@ -18413,6 +18713,59 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
18413
18713
  return value;
18414
18714
  }
18415
18715
  /**
18716
+ * Note what SECRECY the attribute just read carries, then hand it back
18717
+ * UNCHANGED (issue [#2274](https://github.com/go-to-k/cdkd/issues/2274)).
18718
+ *
18719
+ * Two independent notes, in the two directions a `NoEcho` custom-resource
18720
+ * `Data` value travels, and they are on one function because both are read
18721
+ * off the same value at the same instant:
18722
+ *
18723
+ * 1. **This run's fresh value.** If a provider declared this resource's
18724
+ * attributes `NoEcho` earlier in THIS deploy, every string leaf becomes a
18725
+ * MASK-ONLY needle in the CONSUMER's own bag, so the plaintext this
18726
+ * resolution is about to substitute into the dependent's properties is
18727
+ * masked when that record is persisted. Registering into
18728
+ * `context.recordedSecretValues` — the consumer's bag — rather than the
18729
+ * producer's is what makes it work at all: `perResourceSecrets` is keyed by
18730
+ * LOGICAL ID, so a needle recorded under the custom resource's own id is
18731
+ * not in the bag the DEPENDENT's record is scrubbed with.
18732
+ * 2. **A previous run's masked value.** If what came back IS the mask, this
18733
+ * attribute was redacted into `state.json` by an earlier deploy and cdkd
18734
+ * cannot recover it (there is no durable `NoEcho` flag and no expression to
18735
+ * re-resolve — issue #2449). It is recorded as a redacted READ so the
18736
+ * deploy engine can refuse to push the literal `***` to AWS.
18737
+ *
18738
+ * IT RETURNS THE VALUE, and passing through rather than mutating in place is
18739
+ * the point: a new attribute-serving branch is written as `return
18740
+ * this.noteAttributeSecrecy(...)` by imitation of the two that have it.
18741
+ * `Fn::GetAtt` must keep delivering the REAL value (CloudFormation does,
18742
+ * measured), so this can never rewrite what it is handed.
18743
+ *
18744
+ * IT IS NOT A GUARANTEE, and an earlier revision claimed it was ("impossible
18745
+ * to add a third branch that silently skips the note"). Nothing in the type
18746
+ * system stops a branch returning a value it never passed through here, and
18747
+ * one already did: the Route 53 `NameServers` legacy-shape normalization,
18748
+ * which reads the SAME persisted `attributes` bag and shipped without the
18749
+ * note (it takes it now). The rule the note actually needs is about the
18750
+ * SOURCE of the value — every branch serving one out of a PERSISTED
18751
+ * `attributes` bag must call this — and that is not a shape a compiler can
18752
+ * enforce. `constructGuardedAttribute`'s return is deliberately outside it:
18753
+ * that value is fetched from AWS in this run, not read back from state, so
18754
+ * it can be neither a stale mask nor a value a provider declared `NoEcho`.
18755
+ *
18756
+ * A context supplying NEITHER field — the diff / no-op resolver, `cdkd
18757
+ * scrub`, `cdkd import` — pays two undefined checks and gets its value back.
18758
+ */
18759
+ noteAttributeSecrecy(logicalId, attributeName, value, context) {
18760
+ const declared = context.noEchoAttributeResources?.get(logicalId);
18761
+ if ((declared === true || declared !== void 0 && declared.has(attributeName)) && context.recordedSecretValues) recordMaskOnlyValuesIn(value, context.recordedSecretValues);
18762
+ if (context.redactedAttributeReads !== void 0 && carriesSecretMask(value)) {
18763
+ const read = `${logicalId}.${attributeName}`;
18764
+ if (!context.redactedAttributeReads.includes(read)) context.redactedAttributeReads.push(read);
18765
+ }
18766
+ return value;
18767
+ }
18768
+ /**
18416
18769
  * Refuse a pre-#1681 PLACEHOLDER ARN served from the cached attribute map
18417
18770
  * (issue #1729) — the `Fn::GetAtt` half of the guard
18418
18771
  * {@link cfnRefValueFromPhysicalId} applies to `Ref`.
@@ -19376,7 +19729,15 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
19376
19729
  * the producer's rather than the caller's — which is exactly what issue #2057
19377
19730
  * says the other two copies get wrong.
19378
19731
  */
19379
- async reresolveCrossStackValue(value, producerRegion, context, origin, sourceKey) {
19732
+ async reresolveCrossStackValue(value, producerRegion, context, origin, sourceKey, producerOutput) {
19733
+ if (carriesSecretMask(value)) {
19734
+ const recovered = producerOutput === void 0 || producerOutput.crossAccount === true ? void 0 : recoverMaskedOutput(producerOutput.stackName, producerOutput.region, producerOutput.outputKey);
19735
+ if (recovered !== void 0) {
19736
+ if (context.recordedSecretValues) recordMaskOnlyValuesIn(recovered, context.recordedSecretValues);
19737
+ return recovered;
19738
+ }
19739
+ if (context.redactedAttributeReads !== void 0 && !context.redactedAttributeReads.includes(origin)) context.redactedAttributeReads.push(origin);
19740
+ }
19380
19741
  if (!carriesDynamicReference(value)) return value;
19381
19742
  const resolver = this.resolverForProducerRegion(producerRegion);
19382
19743
  const pinnedContext = producerRegion ? withoutProducerRegions(context) : context;
@@ -19497,7 +19858,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
19497
19858
  if (entry && (!context.stackName || entry.producerStack !== context.stackName)) {
19498
19859
  this.recordImport(context, exportName, entry.producerStack, entry.producerRegion);
19499
19860
  this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from index: ${entry.producerStack} / ${entry.producerRegion}; ${carriesDynamicReference(entry.value) ? "redacted dynamic reference" : "literal value"})`);
19500
- return await this.reresolveCrossStackValue(entry.value, entry.producerRegion, context, `Fn::ImportValue '${exportName}' (producer ${entry.producerStack} / ${entry.producerRegion})`, sourceKey);
19861
+ return await this.reresolveCrossStackValue(entry.value, entry.producerRegion, context, `Fn::ImportValue '${exportName}' (producer ${entry.producerStack} / ${entry.producerRegion})`, sourceKey, {
19862
+ stackName: entry.producerStack,
19863
+ region: entry.producerRegion,
19864
+ outputKey: exportName
19865
+ });
19501
19866
  }
19502
19867
  }
19503
19868
  const allStacks = await context.stateBackend.listStacks();
@@ -19544,7 +19909,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
19544
19909
  continue;
19545
19910
  }
19546
19911
  }
19547
- if (found) return await this.reresolveCrossStackValue(found.value, found.lookupRegion, context, `Fn::ImportValue '${exportName}' (producer ${found.refStack} / ${found.lookupRegion})`, sourceKey);
19912
+ if (found) return await this.reresolveCrossStackValue(found.value, found.lookupRegion, context, `Fn::ImportValue '${exportName}' (producer ${found.refStack} / ${found.lookupRegion})`, sourceKey, {
19913
+ stackName: found.refStack,
19914
+ region: found.lookupRegion,
19915
+ outputKey: exportName
19916
+ });
19548
19917
  if (this.cfnFallback) {
19549
19918
  const cfnExport = await this.lookupCfnExport(exportName, context);
19550
19919
  if (cfnExport) {
@@ -19797,7 +20166,12 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
19797
20166
  this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${region}, OutputName=${loggedOutputName}${roleArn ? `, RoleArn=${roleArn}` : ""} (${carriesDynamicReference(value) ? "redacted dynamic reference" : "literal value"})`);
19798
20167
  if (!roleArn) this.recordOutputRead(context, stackName, region, outputName);
19799
20168
  if (roleArn && !context.skipDynamicReferences && carriesDynamicReference(value)) throw markNonRetryable(new CrossAccountSecretRefusalError(`Fn::GetStackOutput: output '${outputName}' of stack '${stackName}' (${region}) is a redacted dynamic reference, and this is a CROSS-ACCOUNT reference (RoleArn ${roleArn}). cdkd will not resolve a producer account's secret with the consumer's credentials — a same-named secret in the consumer account would answer instead. Export a non-secret value (e.g. the secret's ARN) and resolve it in the consumer stack, or reference the producer stack from within its own account.`));
19800
- return await this.reresolveCrossStackValue(value, region, context, `Fn::GetStackOutput '${outputName}' (producer ${stackName} / ${region})`, sourceKey);
20169
+ return await this.reresolveCrossStackValue(value, region, context, `Fn::GetStackOutput '${outputName}' (producer ${stackName} / ${region})`, sourceKey, {
20170
+ stackName,
20171
+ region,
20172
+ outputKey: outputName,
20173
+ ...roleArn ? { crossAccount: true } : {}
20174
+ });
19801
20175
  }
19802
20176
  /**
19803
20177
  * Push a resolved `Fn::GetStackOutput` into the consumer's
@@ -21462,7 +21836,7 @@ var CloudControlProvider = class {
21462
21836
  const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
21463
21837
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
21464
21838
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
21465
- const { ASGProvider } = await import("./asg-provider-CvYAGRMV.js").then((n) => n.n);
21839
+ const { ASGProvider } = await import("./asg-provider-hNkWDuAJ.js").then((n) => n.n);
21466
21840
  return withIndeterminateGuard(await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
21467
21841
  }
21468
21842
  const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
@@ -23738,7 +24112,8 @@ var CustomResourceProvider = class CustomResourceProvider {
23738
24112
  this.logger.debug(`Successfully created custom resource ${logicalId}: ${physicalId}`);
23739
24113
  return {
23740
24114
  physicalId,
23741
- attributes
24115
+ attributes,
24116
+ ...cfnResponse.NoEcho === true && { noEchoAttributes: true }
23742
24117
  };
23743
24118
  } catch (error) {
23744
24119
  const cause = error instanceof Error ? error : void 0;
@@ -23773,7 +24148,8 @@ var CustomResourceProvider = class CustomResourceProvider {
23773
24148
  return {
23774
24149
  physicalId: newPhysicalId,
23775
24150
  wasReplaced,
23776
- attributes
24151
+ attributes,
24152
+ ...cfnResponse.NoEcho === true && { noEchoAttributes: true }
23777
24153
  };
23778
24154
  } catch (error) {
23779
24155
  const cause = error instanceof Error ? error : void 0;
@@ -24285,6 +24661,7 @@ var CustomResourceProvider = class CustomResourceProvider {
24285
24661
  const result = { Status: "SUCCESS" };
24286
24662
  if (payload.PhysicalResourceId) result.PhysicalResourceId = payload.PhysicalResourceId;
24287
24663
  if (payload.Data) result.Data = payload.Data;
24664
+ if (payload.NoEcho === true) result.NoEcho = true;
24288
24665
  return result;
24289
24666
  }
24290
24667
  hasDirectPayload = Object.keys(payload).length > 0;
@@ -29882,6 +30259,37 @@ async function resolveReplayProps(props, resolvers, secrets, execCtx, logicalId)
29882
30259
  return await walk(props, "");
29883
30260
  }
29884
30261
  /**
30262
+ * Refuse to REPLAY a bag whose recorded baseline holds a REDACTION MASK (issue
30263
+ * [#2274](https://github.com/go-to-k/cdkd/issues/2274)).
30264
+ *
30265
+ * The rollback twin of `drift --revert`'s
30266
+ * `preserveLiveValuesAtMaskedLeaves`, and it exists for the same reason: a
30267
+ * `NoEcho` custom resource's `Data` resolved into a dependent's property is
30268
+ * persisted as {@link SECRET_MASK}, because there is no expression to store in
30269
+ * its place — and this executor replays a persisted bag straight to
30270
+ * `provider.update()` / `create()`. Without a guard the literal `***` would be
30271
+ * written onto the live resource, which is the issue #1498 / #1501
30272
+ * data-corruption class.
30273
+ *
30274
+ * IT REFUSES rather than substituting, and that is the difference from the
30275
+ * drift twin. `--revert` holds an AWS-current readback beside the baseline, so
30276
+ * it can leave the position exactly as AWS has it; a replay holds no readback
30277
+ * at all — `previousState.properties` IS its only source — so there is nothing
30278
+ * to fall back to. Failing the ONE op with an actionable message is strictly
30279
+ * better than writing a value cdkd knows is wrong, and the per-op failure
30280
+ * accounting this file already has is what carries it.
30281
+ *
30282
+ * CALLED ON THE WRITTEN SIDE ONLY. Each revert arm resolves two bags; the other
30283
+ * one becomes the provider's `previousProperties`, where a mask is harmless (a
30284
+ * patch provider comparing `***` against the desired value simply sees a
30285
+ * change, which is the correct conclusion — the live value is not what state
30286
+ * records). Refusing there would block rollbacks that have no problem.
30287
+ */
30288
+ function refuseMaskedReplayBaseline(props, logicalId) {
30289
+ if (props === void 0 || !carriesSecretMask(props)) return;
30290
+ throw new CdkdError(`Cannot roll ${logicalId} back: its recorded baseline holds the redaction mask ('${"***"}') where a NoEcho custom-resource value was resolved, so cdkd would write that literal to the live resource. Restore the property with 'cdkd deploy' AFTER forcing that custom resource to update (change one of its properties, e.g. a nonce), so its handler runs again and supplies the real value — an ordinary re-deploy leaves the resource unchanged, so the handler does not run and the mask stays. See https://github.com/go-to-k/cdkd/issues/2449.`, "ROLLBACK_REDACTED_BASELINE");
30291
+ }
30292
+ /**
29885
30293
  * The replay's resolvers: the stack's own, plus one pinned sibling per FOREIGN
29886
30294
  * region an ARN-named reference asks for (issue #2057).
29887
30295
  *
@@ -30336,6 +30744,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
30336
30744
  const current = stateResources[op.logicalId];
30337
30745
  const prev = op.previousState;
30338
30746
  const resolvedPrevProps = await resolveReplayProps(prev.properties, resolver, secrets, ctx, op.logicalId) ?? {};
30747
+ refuseMaskedReplayBaseline(resolvedPrevProps, op.logicalId);
30339
30748
  recordNestedStackParameterExpressions(secrets, op.resourceType, resolvedPrevProps, prev.properties, STATE_DERIVED_RULES);
30340
30749
  logger.info(` Rollback: Reversing replacement of ${op.logicalId} (${op.resourceType}) — re-creating the old resource and deleting the new one`);
30341
30750
  if (STATEFUL_TYPES.has(op.resourceType)) logger.warn(` ⚠ ${op.logicalId} (${op.resourceType}) is a stateful type — the old physical resource's data was destroyed by the replacement and CANNOT be recovered; the re-created resource starts empty.`);
@@ -30429,6 +30838,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
30429
30838
  provisionedBy: op.provisionedBy
30430
30839
  });
30431
30840
  const desiredProps = await resolveReplayProps(previousState.properties, resolver, secrets, ctx, op.logicalId);
30841
+ refuseMaskedReplayBaseline(desiredProps, op.logicalId);
30432
30842
  const currentProps = await resolveReplayProps(current.properties, resolver, secrets, ctx, op.logicalId);
30433
30843
  recordNestedStackParameterExpressions(secrets, op.resourceType, desiredProps, previousState.properties, STATE_DERIVED_RULES);
30434
30844
  const revertResult = await updateWithRollbackRetry(provider, [
@@ -30585,6 +30995,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
30585
30995
  provisionedBy: op.provisionedBy ?? current.provisionedBy
30586
30996
  });
30587
30997
  const desiredProps = await resolveReplayProps(prev.properties, resolver, secrets, ctx, op.logicalId);
30998
+ refuseMaskedReplayBaseline(desiredProps, op.logicalId);
30588
30999
  const attemptedProps = await resolveReplayProps(op.attemptedProperties ?? current.properties, resolver, secrets, ctx, op.logicalId);
30589
31000
  recordNestedStackParameterExpressions(secrets, op.resourceType, desiredProps, prev.properties, STATE_DERIVED_RULES);
30590
31001
  const revertFailedResult = await updateWithRollbackRetry(provider, [
@@ -31431,6 +31842,29 @@ var DeployEngine = class {
31431
31842
  */
31432
31843
  perResourceSecrets = /* @__PURE__ */ new Map();
31433
31844
  /**
31845
+ * Logical ids whose provider declared THIS RUN's `attributes` sensitive
31846
+ * (`ResourceCreateResult.noEchoAttributes` — issue
31847
+ * [#2274](https://github.com/go-to-k/cdkd/issues/2274)). One producer today:
31848
+ * `CustomResourceProvider`, relaying the handler's `NoEcho: true`.
31849
+ *
31850
+ * IN-RUN ONLY, and that is the whole shape of the feature rather than a
31851
+ * shortcut. `NoEcho` arrives on a RESPONSE, so cdkd knows it exactly when the
31852
+ * handler answered — this deploy — and `ResourceState` carries no durable
31853
+ * per-attribute flag to remember it by (a v9 -> v10 schema bump, issue
31854
+ * [#2449](https://github.com/go-to-k/cdkd/issues/2449)). Within the run that
31855
+ * is enough: the DAG provisions the custom resource before anything that
31856
+ * depends on it, so every dependent's resolution sees the entry. Across runs
31857
+ * the persisted `***` is the signal instead — see
31858
+ * `ResolverContext.redactedAttributeReads`.
31859
+ *
31860
+ * Reset per `deploy()`, like `perResourceSecrets`.
31861
+ *
31862
+ * `true` means the WHOLE attributes bag is sensitive (a custom resource's
31863
+ * `NoEcho` response); a SET names the sensitive members only (a nested
31864
+ * stack's `Outputs.<Key>` entries — see `NoEchoAttributesResult`).
31865
+ */
31866
+ noEchoAttributeResources = /* @__PURE__ */ new Map();
31867
+ /**
31434
31868
  * PER-RESOURCE unresolved TEMPLATE properties, keyed by logicalId (issues
31435
31869
  * #1904 / #1900). The redaction choke point uses this as the POSITION source:
31436
31870
  * wherever the template leaf is a `{{resolve:...}}` string, state persists
@@ -31518,6 +31952,7 @@ var DeployEngine = class {
31518
31952
  this.recordedImports = [];
31519
31953
  this.recordedOutputReads = [];
31520
31954
  this.perResourceSecrets = /* @__PURE__ */ new Map();
31955
+ this.noEchoAttributeResources = /* @__PURE__ */ new Map();
31521
31956
  this.perResourceTemplateProps = /* @__PURE__ */ new Map();
31522
31957
  this.outputSecrets = /* @__PURE__ */ new Map();
31523
31958
  this.outputsTemplateSource = {};
@@ -31545,7 +31980,9 @@ var DeployEngine = class {
31545
31980
  recordedImports: this.recordedImports,
31546
31981
  recordedOutputReads: this.recordedOutputReads,
31547
31982
  ...this.options.inheritedSecrets && this.options.inheritedSecrets.size > 0 && { inheritedSecrets: this.options.inheritedSecrets },
31548
- recordedSecretValues
31983
+ recordedSecretValues,
31984
+ noEchoAttributeResources: this.noEchoAttributeResources,
31985
+ redactedAttributeReads: []
31549
31986
  };
31550
31987
  }
31551
31988
  /**
@@ -31594,6 +32031,30 @@ var DeployEngine = class {
31594
32031
  * resolving one secret collapsed onto whichever expression was recorded last
31595
32032
  * at all three.
31596
32033
  */
32034
+ /**
32035
+ * Record the plaintext behind every output {@link redactOutputs} just masked,
32036
+ * for the duration of THIS PROCESS (issue #2274).
32037
+ *
32038
+ * Per KEY, comparing the two bags rather than re-deriving from the secrets
32039
+ * map: what matters is whether the persisted value at this key IS a mask that
32040
+ * the resolved value was not, which is exactly "this key's plaintext is about
32041
+ * to become unreadable". A key already carrying `***` before redaction — a
32042
+ * value read back out of a previous run's state — is skipped, because there
32043
+ * is no plaintext behind it to remember.
32044
+ *
32045
+ * Called only from the REAL-DEPLOY outputs pass. The other two `redactOutputs`
32046
+ * callers hand it a bag from a previous generation, where a mask is already
32047
+ * unrecoverable and pretending otherwise would serve a stale value.
32048
+ */
32049
+ rememberRecoverableMaskedOutputs(stackName, resolved, redacted) {
32050
+ if (resolved === redacted) return;
32051
+ for (const [key, redactedValue] of Object.entries(redacted)) {
32052
+ if (!carriesSecretMask(redactedValue)) continue;
32053
+ const plaintext = resolved[key];
32054
+ if (plaintext === void 0 || carriesSecretMask(plaintext)) continue;
32055
+ recordRecoverableMaskedOutput(stackName, this.stackRegion, key, plaintext);
32056
+ }
32057
+ }
31597
32058
  redactOutputs(outputs) {
31598
32059
  if (this.outputSecrets.size === 0) return outputs;
31599
32060
  return redactSecretsForState(outputs, this.outputSecrets, this.outputsSourceUsable ? this.outputsTemplateSource : void 0, TEMPLATE_SOURCED_RULES);
@@ -31625,6 +32086,85 @@ var DeployEngine = class {
31625
32086
  * same shape — so it is redacted against ITSELF via `scrubResourceRecord`,
31626
32087
  * the same #1900 fallback an UNCHANGED resource takes.
31627
32088
  */
32089
+ /**
32090
+ * Take a provider's `noEchoAttributes` declaration and turn it into REDACTION
32091
+ * (issue [#2274](https://github.com/go-to-k/cdkd/issues/2274)).
32092
+ *
32093
+ * TWO registrations, and both are needed because `perResourceSecrets` is
32094
+ * keyed by LOGICAL ID:
32095
+ *
32096
+ * - the values go into the PRODUCER's own bag, which is what
32097
+ * `scrubResourceRecord` redacts this record's `attributes` with;
32098
+ * - the logical id goes into {@link noEchoAttributeResources}, which every
32099
+ * later resolution consults, so a DEPENDENT that resolves an `Fn::GetAtt`
32100
+ * here records the same plaintext into ITS bag and its resolved
32101
+ * `properties` are masked too. Without the second half the custom resource's
32102
+ * record would be clean while the SSM parameter that consumed it still held
32103
+ * the plaintext — a line that cannot be explained to someone who set
32104
+ * `NoEcho` expecting "not in state".
32105
+ *
32106
+ * Called AFTER the provider returns and BEFORE the state record is built, so
32107
+ * the needles exist by the time anything is persisted. Nothing is masked in
32108
+ * memory: `stateResources[logicalId].attributes` keeps the REAL value, which
32109
+ * is what the resolver serves to dependents in this same run.
32110
+ *
32111
+ * `ownProperties` is the resource's OWN resolved template bag, and passing it
32112
+ * is what stops a handler from masking cdkd's inputs back at it (issue #2274
32113
+ * review). Its whole string leaves are EXCLUDED from the needles: a handler
32114
+ * echoing `event.ResourceProperties` into its `Data` — the shape the CDK
32115
+ * `Provider` samples encourage — makes `Data.X` equal the resource's own
32116
+ * `ServiceToken`, and registering that rewrites `properties.ServiceToken` to
32117
+ * `***` in the record `CustomResourceProvider.delete` reads it back from,
32118
+ * where the mask is a truthy string that passes both of that method's guards.
32119
+ * A value already present in the template is not handler-GENERATED, so
32120
+ * excluding it gives up no secrecy — and where the template value IS a
32121
+ * resolved secret it already carries a real EXPRESSION needle, which
32122
+ * `recordMaskOnlyValue` would refuse to demote anyway.
32123
+ */
32124
+ registerNoEchoAttributes(logicalId, result, secrets, ownProperties) {
32125
+ const attributes = result.attributes;
32126
+ if (attributes === void 0) return;
32127
+ const excluded = ownProperties === void 0 ? void 0 : wholeStringLeavesOf(ownProperties);
32128
+ if (result.noEchoAttributes === true) {
32129
+ this.noEchoAttributeResources.set(logicalId, true);
32130
+ recordMaskOnlyValuesIn(attributes, secrets, excluded);
32131
+ return;
32132
+ }
32133
+ const names = (result.noEchoAttributeNames ?? []).filter((name) => name in attributes);
32134
+ if (names.length === 0) return;
32135
+ this.noEchoAttributeResources.set(logicalId, new Set(names));
32136
+ for (const name of names) recordMaskOnlyValuesIn(attributes[name], secrets, excluded);
32137
+ }
32138
+ /**
32139
+ * Refuse to provision a resource whose resolution served a REDACTED attribute
32140
+ * out of a previous deploy's state (issue #2274).
32141
+ *
32142
+ * The unavoidable cost of masking a `NoEcho` custom resource's `Data`: state
32143
+ * then holds `***`, and cdkd cannot get the value back without re-invoking
32144
+ * the handler, which is a SIDE-EFFECTING operation it must not perform just
32145
+ * to fill in a property. Since `ResourceState` carries no durable `NoEcho`
32146
+ * flag (issue #2449), there is not even a way to tell the user which
32147
+ * attribute it was without this record.
32148
+ *
32149
+ * REFUSING IS THE SAFE DIRECTION and the alternative is not "it works": the
32150
+ * literal `***` would be written to the live resource by any provider that
32151
+ * sends its desired bag wholesale (`PutParameter` and every
32152
+ * `Put*Configuration`), which is the issue #1498 / #1501 data-corruption
32153
+ * class. A loud failure naming the remedy is strictly better than a silent
32154
+ * wrong write.
32155
+ *
32156
+ * NARROW BY CONSTRUCTION. The bag is only non-empty when a `Fn::GetAtt`
32157
+ * actually served a masked attribute during THIS resource's resolution, so a
32158
+ * resource whose properties merely happen to contain the string `***` is
32159
+ * untouched — which is why the check is not "does `resolvedProps` hold the
32160
+ * mask". And the diff pass does not consult the bag at all, so an untouched
32161
+ * stack still reports NO_CHANGE and deploys.
32162
+ */
32163
+ refuseRedactedAttributeReads(logicalId, resourceType, context) {
32164
+ const reads = context.redactedAttributeReads;
32165
+ if (reads === void 0 || reads.length === 0) return;
32166
+ throw new ProvisioningError(`Cannot resolve ${reads.join(", ")} for ${logicalId}: cdkd's recorded state holds only the redaction mask there, and the value is not recoverable from state. That happens when a custom resource handler declared its response NoEcho: true — the value is generated by the handler, so cdkd has nothing to re-derive it from and must not write the literal mask to AWS. Two remedies: force that custom resource to update (change one of its properties, e.g. a nonce / version property) so its handler runs again and supplies the value in this same run; or stop setting NoEcho on that response. If the value comes from ANOTHER stack, the producer and this stack must deploy in ONE run (cdkd deploy --all) with the producer's custom resource actually running — re-deploying the producer by itself does not help, because it re-masks the value on the way into its own state. See https://github.com/go-to-k/cdkd/issues/2449.`, resourceType, logicalId);
32167
+ }
31628
32168
  redactOperationsForJournal(operations) {
31629
32169
  return operations.map((op) => {
31630
32170
  const secrets = this.perResourceSecrets.get(op.logicalId);
@@ -32272,7 +32812,9 @@ var DeployEngine = class {
32272
32812
  let outputs;
32273
32813
  try {
32274
32814
  outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
32815
+ const resolvedOutputsBeforeRedaction = outputs;
32275
32816
  outputs = this.redactOutputs(outputs);
32817
+ this.rememberRecoverableMaskedOutputs(stackName, resolvedOutputsBeforeRedaction, outputs);
32276
32818
  } catch (outputError) {
32277
32819
  await this.persistStateAfterOutputFailure(stackName, currentState, newResources, currentEtag, pendingMigration);
32278
32820
  await this.writeRollbackJournalSegment(stackName, completedOperations, failedOperations, "no-rollback-failure", currentEtag === void 0);
@@ -32688,6 +33230,7 @@ var DeployEngine = class {
32688
33230
  }, stackName);
32689
33231
  if (context.recordedSecretValues) this.perResourceSecrets.set(logicalId, context.recordedSecretValues);
32690
33232
  const resolvedProps = await this.resolver.resolve(desiredProps, context);
33233
+ this.refuseRedactedAttributeReads(logicalId, resourceType, context);
32691
33234
  this.perResourceTemplateProps.set(logicalId, desiredProps);
32692
33235
  const createSecrets = context.recordedSecretValues ?? /* @__PURE__ */ new Map();
32693
33236
  recordNestedStackParameterExpressions(createSecrets, resourceType, resolvedProps, desiredProps);
@@ -32700,6 +33243,7 @@ var DeployEngine = class {
32700
33243
  const createProvider = createDecision.provider;
32701
33244
  const createProps = createDecision.provisionedBy === "cc-api" ? this.preparePropertiesForCcApi(resourceType, resolvedProps, logicalId) : resolvedProps;
32702
33245
  const result = await this.withRetry(() => withCurrentResourceSecrets(createSecrets, () => createProvider.create(logicalId, resourceType, createProps, { maskSecrets: createSecretMasker(createSecrets) })), logicalId, void 0, void 0, createProvider);
33246
+ this.registerNoEchoAttributes(logicalId, result, createSecrets, resolvedProps);
32703
33247
  const dependencies = this.extractAllDependencies(template, logicalId);
32704
33248
  const templateAttrs = this.extractTemplateAttributes(template, logicalId);
32705
33249
  stateResources[logicalId] = {
@@ -32734,6 +33278,7 @@ var DeployEngine = class {
32734
33278
  const updateSecrets = context.recordedSecretValues ?? /* @__PURE__ */ new Map();
32735
33279
  this.perResourceSecrets.set(logicalId, updateSecrets);
32736
33280
  const resolvedProps = await this.resolver.resolve(desiredProps, context);
33281
+ this.refuseRedactedAttributeReads(logicalId, resourceType, context);
32737
33282
  this.perResourceTemplateProps.set(logicalId, desiredProps);
32738
33283
  recordNestedStackParameterExpressions(updateSecrets, resourceType, resolvedProps, desiredProps);
32739
33284
  this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
@@ -32875,6 +33420,7 @@ var DeployEngine = class {
32875
33420
  }
32876
33421
  }
32877
33422
  }
33423
+ this.registerNoEchoAttributes(logicalId, createResult, updateSecrets, resolvedProps);
32878
33424
  stateResources[logicalId] = {
32879
33425
  physicalId: createResult.physicalId,
32880
33426
  resourceType,
@@ -32950,6 +33496,11 @@ var DeployEngine = class {
32950
33496
  }
32951
33497
  if (result.wasReplaced) this.logger.info(`Resource ${logicalId} was replaced: ${currentResource.physicalId} -> ${result.physicalId}`);
32952
33498
  const carriedAttributes = result.attributes ?? (result.wasReplaced ? void 0 : currentResource.attributes);
33499
+ this.registerNoEchoAttributes(logicalId, {
33500
+ ...carriedAttributes && { attributes: carriedAttributes },
33501
+ ...result.noEchoAttributes === true && { noEchoAttributes: true },
33502
+ ...result.noEchoAttributeNames && { noEchoAttributeNames: result.noEchoAttributeNames }
33503
+ }, updateSecrets, resolvedProps);
32953
33504
  stateResources[logicalId] = {
32954
33505
  physicalId: result.physicalId,
32955
33506
  resourceType,
@@ -33406,5 +33957,5 @@ var DeployEngine = class {
33406
33957
  };
33407
33958
 
33408
33959
  //#endregion
33409
- export { DEFAULT_STATE_PREFIX as $, CFN_TEMPLATE_BODY_LIMIT as $n, maskSecretsInError as $t, bold as A, describeAwsFailure as An, NestedStackChildDirectDestroyError as Ar, requireConfigObject as At, secretBearingStateKeyWarning as B, Synthesizer as Bn, isCdkdError as Br, DiffCalculator as Bt, unsupportedFinalSnapshotError as C, getBootstrapMarkerKey as Cn, DeployCancelledError as Cr, assertRegionMatch as Ct, isStatefulRecreateTargetSync as D, validateAssetBucketName as Dn, LocalStartServiceError as Dr, readConfigString as Dt, MULTI_REGION_RECREATE_BLOCKED_TYPES as E, readBootstrapMarkerBody as En, LocalMigrateError as Er, configStringRefusal as Et, yellow as F, partitionSensitiveEnv as Fn, StackHasActiveImportsError as Fr, s3BucketDomainName as Ft, clearOnUpdateRemoval as G, resolveAutoAssetStorage as Gn, isThrottlingError as Gr, TemplateParser as Gt, getCurrentResourceSecrets as H, getDefaultStateBucketName as Hn, withErrorHandling as Hr, describeTypeWithThrottleRetry as Ht, collectDeclaredOutputNames as I, runDockerForeground as In, StackTerminationProtectionError as Ir, s3BucketDualStackDomainName as It, findSilentDropProperties as J, resolveStateBucketWithDefault as Jn, markRedactedCause as Jr, TEMPLATE_SOURCED_RULES as Jt, ProviderRegistry as K, resolveCaptureObservedState as Kn, isTransientServerError as Kr, STATE_SOURCED_CROSS_GENERATION_RULES as Kt, collectPublishedOutputNames as L, runDockerStreaming as Ln, StateError as Lr, s3BucketRegionalDomainName as Lt, gray as M, dockerSpawnEnvWithSensitive as Mn, ProvisioningError as Mr, classifyReplaySecretRegion as Mt, green as N, formatDockerLoginError as Nn, ResourceTimeoutError as Nr, producerRegionsFromState as Nt, renderStatefulReason as O, validateContainerRepoName as On, LockError as Or, replayWarn as Ot, red as P, getDockerCmd as Pn, ResourceUpdateNotSupportedError as Pr, s3BucketArn as Pt, CUSTOM_RESOURCE_RESPONSE_PREFIX as Q, warnDeprecatedNoPrefixCliFlag as Qn, isSingleDynamicReferenceToken as Qt, exportAliasCollisionScrubWarning as R, AssetManifestLoader as Rn, SynthesisError as Rr, s3BucketWebsiteUrl as Rt, refusesFinalSnapshot as S, ensureAssetStorage as Sn, DependencyError as Sr, resolveExplicitPhysicalId as St, extractDeploymentEventError as T, parseBootstrapMarker as Tn, LocalInvokeBuildError as Tr, configBooleanRefusal as Tt, IAMRoleProvider as U, getLegacyStateBucketName as Un, isMarkedNonRetryable as Ur, withRetry as Ut, stateKeySecretExposure as V, synthesisStatusMessage as Vn, normalizeAwsError as Vr, INTRINSIC_KEYS as Vt, collectInlinePolicyNamesManagedBySiblings as W, resolveApp as Wn, isRetryableTransientError as Wr, DagBuilder as Wt, maskDeep as X, resolveUseCdkBootstrapAssets as Xn, __exportAll as Xr, dynamicReferenceTokens as Xt, createMaskedRetryLogger as Y, resolveStateBucketWithDefaultAndSource as Yn, retryClassificationText as Yr, createSecretMasker as Yt, maskerOrIdentity as Z, stateBucketExistenceConfirmed as Zn, errorCauseChain as Zt, PRE_DELETE_SNAPSHOT_TYPES as _, escapeRegExp$1 as _n, setAwsClients as _r, isUnboundTemplateParameter as _t, DeploymentEventsStore as a, rebuildClientForBucketRegion as an, displaySafe as ar, CloudControlProvider as at, createPreDeleteFinalSnapshot as b, BOOTSTRAP_MARKER_PREFIX as bn, ConfigError as br, WAFv2WebACLProvider as bt, replayFailedOperations as c, importableOutputs as cn, canonicalizeRegion as cr, deleteIndeterminateGuards as ct, updatePartialReason as d, stringifyValue as dn, processStackMessages as dr, isTerminationProtectionPropagationError as dt, maskSecretsInText as en, CFN_TEMPLATE_URL_LIMIT as er, beginCommandInterruptScope as et, withResourceDeadline as f, WorkGraph as fn, clearBucketRegionCache as fr, IntrinsicFunctionResolver as ft, ATOMIC_FINAL_SNAPSHOT_TYPES as g, rewriteTemplateAssetReferences as gn, resetAwsClients as gr, getAccountInfo as gt, computeImplicitDeleteEdges as h, loadPublishableAssetManifest as hn, getAwsClients as hr, coerceParameterTypedValue as ht, DeploymentEventsReader as i, S3StateBackend as in, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as ir, startInterruptWatch as it, cyan as j, buildDockerImage as jn, PartialFailureError as jr, requireConfigString as jt, formatResourceLine as k, buildDenyExternalAccessPolicy as kn, MissingCdkCliError as kr, requireConfigArray as kt, replayRollback as l, shouldRetainResource as ln, derivePartitionAndUrlSuffix as lr, deleteSkipReason as lt, IMPLICIT_DELETE_DEPENDENCIES as m, createAssetRedirectResolver as mn, AwsClients as mr, cfnRefValueFromPhysicalId as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, scrubResourceRecord as nn, findLargeInlineResources as nr, interruptWatchListenerCount as nt, planFailedOps as o, exportNamesCarriedFrom as on, expectedOwnerParam as or, slowCcOperationTimeoutMs as ot, maskingRetryLogger as p, buildAssetRedirectMap as pn, resolveBucketRegion as pr, carriesDynamicReference as pt, findActionableSilentDrops as q, resolveSkipPrefix as qn, markNonRetryable as qr, STATE_SOURCED_READBACK_RULES as qt, DeployEngine as r, LockManager as rn, uploadCfnTemplate as rr, isInterruptedWaitError as rt, planRollback as s, importableOutputKeys as sn, PARTITION_TABLE as sr, UNSPECIFIED_SKIP_REASON as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, redactSecretsForState as tn, MIGRATE_TMP_PREFIX as tr, endCommandInterruptScope as tt, updatePartialMessage as u, AssetPublisher as un, AssemblyReader as ur, disableInstanceApiTermination as ut, buildFinalSnapshotIdentifier as v, stripControlChars as vn, AssetError as vr, parameterTypeMayLoseSecretIdentity as vt, makeCanonicalizePropertiesFn as w, isCrossRegionRedirect as wn, DynamicReferenceRegionAmbiguousError as wr, coerceCfnBoolean as wt, isFinalSnapshotError as x, assertAssetBucketRegion as xn, CrossAccountSecretRefusalError as xr, normalizeAwsTagsToCfn as xt, ccRoutedFinalSnapshotError as y, AssetModeResolver as yn, CdkdError as yr, refStateLookupFromResource as yt, isExportAliasCollision as z, getDockerImageBySourceHash as zn, formatError as zr, applyRoleArnIfSet as zt };
33410
- //# sourceMappingURL=deploy-engine-D4QA_B1V.js.map
33960
+ export { DEFAULT_STATE_PREFIX as $, resolveUseCdkBootstrapAssets as $n, __exportAll as $r, isSingleDynamicReferenceToken as $t, bold as A, validateAssetBucketName as An, LocalStartServiceError as Ar, requireConfigObject as At, secretBearingStateKeyWarning as B, runDockerStreaming as Bn, StateError as Br, DiffCalculator as Bt, unsupportedFinalSnapshotError as C, BOOTSTRAP_MARKER_PREFIX as Cn, ConfigError as Cr, assertRegionMatch as Ct, isStatefulRecreateTargetSync as D, isCrossRegionRedirect as Dn, DynamicReferenceRegionAmbiguousError as Dr, readConfigString as Dt, MULTI_REGION_RECREATE_BLOCKED_TYPES as E, getBootstrapMarkerKey as En, DeployCancelledError as Er, configStringRefusal as Et, yellow as F, dockerSpawnEnvWithSensitive as Fn, ProvisioningError as Fr, s3BucketDomainName as Ft, clearOnUpdateRemoval as G, getDefaultStateBucketName as Gn, withErrorHandling as Gr, TemplateParser as Gt, getCurrentResourceSecrets as H, getDockerImageBySourceHash as Hn, formatError as Hr, describeTypeWithThrottleRetry as Ht, collectDeclaredOutputNames as I, formatDockerLoginError as In, ResourceTimeoutError as Ir, s3BucketDualStackDomainName as It, findSilentDropProperties as J, resolveAutoAssetStorage as Jn, isThrottlingError as Jr, TEMPLATE_SOURCED_RULES as Jt, ProviderRegistry as K, getLegacyStateBucketName as Kn, isMarkedNonRetryable as Kr, STATE_SOURCED_CROSS_GENERATION_RULES as Kt, collectPublishedOutputNames as L, getDockerCmd as Ln, ResourceUpdateNotSupportedError as Lr, s3BucketRegionalDomainName as Lt, gray as M, buildDenyExternalAccessPolicy as Mn, MissingCdkCliError as Mr, classifyReplaySecretRegion as Mt, green as N, describeAwsFailure as Nn, NestedStackChildDirectDestroyError as Nr, producerRegionsFromState as Nt, renderStatefulReason as O, parseBootstrapMarker as On, LocalInvokeBuildError as Or, replayWarn as Ot, red as P, buildDockerImage as Pn, PartialFailureError as Pr, s3BucketArn as Pt, CUSTOM_RESOURCE_RESPONSE_PREFIX as Q, resolveStateBucketWithDefaultAndSource as Qn, retryClassificationText as Qr, errorCauseChain as Qt, exportAliasCollisionScrubWarning as R, partitionSensitiveEnv as Rn, StackHasActiveImportsError as Rr, s3BucketWebsiteUrl as Rt, refusesFinalSnapshot as S, AssetModeResolver as Sn, CdkdError as Sr, resolveExplicitPhysicalId as St, extractDeploymentEventError as T, ensureAssetStorage as Tn, DependencyError as Tr, configBooleanRefusal as Tt, IAMRoleProvider as U, Synthesizer as Un, isCdkdError as Ur, withRetry as Ut, stateKeySecretExposure as V, AssetManifestLoader as Vn, SynthesisError as Vr, INTRINSIC_KEYS as Vt, collectInlinePolicyNamesManagedBySiblings as W, synthesisStatusMessage as Wn, normalizeAwsError as Wr, DagBuilder as Wt, maskDeep as X, resolveSkipPrefix as Xn, markNonRetryable as Xr, createSecretMasker as Xt, createMaskedRetryLogger as Y, resolveCaptureObservedState as Yn, isTransientServerError as Yr, carriesSecretMask as Yt, maskerOrIdentity as Z, resolveStateBucketWithDefault as Zn, markRedactedCause as Zr, dynamicReferenceTokens as Zt, PRE_DELETE_SNAPSHOT_TYPES as _, createAssetRedirectResolver as _n, AwsClients as _r, isUnboundTemplateParameter as _t, DeploymentEventsStore as a, scrubResourceRecord as an, findLargeInlineResources as ar, CloudControlProvider as at, createPreDeleteFinalSnapshot as b, escapeRegExp$1 as bn, setAwsClients as br, WAFv2WebACLProvider as bt, replayFailedOperations as c, rebuildClientForBucketRegion as cn, displaySafe as cr, deleteIndeterminateGuards as ct, updatePartialReason as d, importableOutputs as dn, canonicalizeRegion as dr, isTerminationProtectionPropagationError as dt, maskSecretsInError as en, stateBucketExistenceConfirmed as er, beginCommandInterruptScope as et, withResourceDeadline as f, shouldRetainResource as fn, derivePartitionAndUrlSuffix as fr, IntrinsicFunctionResolver as ft, ATOMIC_FINAL_SNAPSHOT_TYPES as g, buildAssetRedirectMap as gn, resolveBucketRegion as gr, getAccountInfo as gt, computeImplicitDeleteEdges as h, WorkGraph as hn, clearBucketRegionCache as hr, coerceParameterTypedValue as ht, DeploymentEventsReader as i, redactSecretsForState as in, MIGRATE_TMP_PREFIX as ir, startInterruptWatch as it, cyan as j, validateContainerRepoName as jn, LockError as jr, requireConfigString as jt, formatResourceLine as k, readBootstrapMarkerBody as kn, LocalMigrateError as kr, requireConfigArray as kt, replayRollback as l, exportNamesCarriedFrom as ln, expectedOwnerParam as lr, deleteSkipReason as lt, IMPLICIT_DELETE_DEPENDENCIES as m, stringifyValue as mn, processStackMessages as mr, cfnRefValueFromPhysicalId as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, recordMaskOnlyValue as nn, CFN_TEMPLATE_BODY_LIMIT as nr, interruptWatchListenerCount as nt, planFailedOps as o, LockManager as on, uploadCfnTemplate as or, slowCcOperationTimeoutMs as ot, maskingRetryLogger as p, AssetPublisher as pn, AssemblyReader as pr, carriesDynamicReference as pt, findActionableSilentDrops as q, resolveApp as qn, isRetryableTransientError as qr, STATE_SOURCED_READBACK_RULES as qt, DeployEngine as r, recoverMaskedOutput as rn, CFN_TEMPLATE_URL_LIMIT as rr, isInterruptedWaitError as rt, planRollback as s, S3StateBackend as sn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as sr, UNSPECIFIED_SKIP_REASON as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, maskSecretsInText as tn, warnDeprecatedNoPrefixCliFlag as tr, endCommandInterruptScope as tt, updatePartialMessage as u, importableOutputKeys as un, PARTITION_TABLE as ur, disableInstanceApiTermination as ut, buildFinalSnapshotIdentifier as v, loadPublishableAssetManifest as vn, getAwsClients as vr, parameterTypeMayLoseSecretIdentity as vt, makeCanonicalizePropertiesFn as w, assertAssetBucketRegion as wn, CrossAccountSecretRefusalError as wr, coerceCfnBoolean as wt, isFinalSnapshotError as x, stripControlChars as xn, AssetError as xr, normalizeAwsTagsToCfn as xt, ccRoutedFinalSnapshotError as y, rewriteTemplateAssetReferences as yn, resetAwsClients as yr, refStateLookupFromResource as yt, isExportAliasCollision as z, runDockerForeground as zn, StackTerminationProtectionError as zr, applyRoleArnIfSet as zt };
33961
+ //# sourceMappingURL=deploy-engine-BhBR2d3w.js.map