@go-to-k/cdkd 0.284.67 → 0.284.69

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,5 +1,5 @@
1
1
  import { d as generateResourceName, f as generateResourceNameWithFallback, g as withStackName, m as looksLikeCdkdGeneratedName, n as getLogger, o as getLiveRenderer, p as getCurrentStackName, u as applyDefaultNameForFallback } from "./logger-C9-E73FI.js";
2
- import { t as getCdkdVersion } from "./version-DNFFsbnH.js";
2
+ import { t as getCdkdVersion } from "./version-C-t2pnSY.js";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -12545,10 +12545,16 @@ function isUniquelyKeyedBy(items, key) {
12545
12545
  * the unchanged-resource path the value scan is also a no-op, so such a leaf
12546
12546
  * keeps its plaintext — narrow (an identity field is a name, not a
12547
12547
  * credential) but real.
12548
- * - **Arrays of arrays.** `M: [[{Name, Value}]]` pairs nothing, because the
12548
+ * - **Arrays of arrays.** `M: [[{Name, Value}]]` pairs nothing HERE, because the
12549
12549
  * OUTER elements are arrays rather than plain objects and have no identity
12550
- * field to key on. Positional descent into the outer list would reintroduce
12551
- * the order assumption this function exists to avoid.
12550
+ * field to key on. Blind positional descent into the outer list would
12551
+ * reintroduce the order assumption this function exists to avoid. On the
12552
+ * READBACK paths it is no longer a dead end:
12553
+ * {@link refuseUncertifiedReadbackPositions} (issue #2012) walks the outer
12554
+ * list positionally when {@link unkeyedArrayPairsByAnchors} says the inner
12555
+ * elements' own anchors vouch for the alignment, which meets the order
12556
+ * objection instead of ignoring it. The gate decides; it does not walk. Everywhere else — and whenever
12557
+ * those anchors do not match — the shape still falls to the value scan.
12552
12558
  */
12553
12559
  function identityKeyFor(bag, source) {
12554
12560
  for (const key of ARRAY_IDENTITY_KEYS) if (isUniquelyKeyedBy(bag, key) && isUniquelyKeyedBy(source, key)) return key;
@@ -12822,6 +12828,286 @@ function mixedLeafMayCarryPublicReference(source, secrets) {
12822
12828
  return dynamicReferenceTokens(source).some((token) => token.startsWith("{{resolve:ssm:") && !isRecordedSecretExpression(token));
12823
12829
  }
12824
12830
  /**
12831
+ * Does this value carry an ORDINARY object prototype?
12832
+ *
12833
+ * `isPlainObject` answers `typeof === 'object' && !Array.isArray`, which admits
12834
+ * CLASS INSTANCES, and that is a hole under {@link deepEqualJsonValue}: an AWS
12835
+ * SDK v3 readback reaching `drainObservedCaptures` is PRE-JSON and really does
12836
+ * carry `Date` values (`LastModified`, `CreationDate`). `Object.keys(new
12837
+ * Date())` is `[]`, so without this check a `Date` compared equal to `{}` and
12838
+ * to every other `Date` -- an anchor that corroborates a pairing while proving
12839
+ * nothing. Widening `isPlainObject` itself was rejected: it is read by three
12840
+ * other walks whose behaviour would change with it, and the defect is in what
12841
+ * EQUALITY means here, not in what counts as a container.
12842
+ *
12843
+ * A `Date` anchor now corroborates NOTHING, so an element carrying one refuses
12844
+ * rather than pairs. That is the conservative direction this module always
12845
+ * takes -- the residual stays a refusal -- and it is stated because the
12846
+ * opposite reading (that a `Date` on both sides is evidence) is the one a
12847
+ * future edit will be tempted by.
12848
+ */
12849
+ function hasPlainPrototype(value) {
12850
+ const proto = Object.getPrototypeOf(value);
12851
+ return proto === Object.prototype || proto === null;
12852
+ }
12853
+ /**
12854
+ * Structural equality over the JSON shapes this module walks (issue #2012).
12855
+ *
12856
+ * Own ENUMERABLE keys only, and a key count on both sides, so an inherited
12857
+ * field is not equality and neither is a bag that merely CONTAINS the source's
12858
+ * keys. That agrees with the two walks beside it -- `isUniquelyKeyedBy` and the
12859
+ * object arm of {@link refuseUncertifiedReadbackPositions} both use
12860
+ * `Object.hasOwn` -- and it matters here rather than being hygiene: this
12861
+ * predicate is the evidence an anchor pairing rests on, so a comparison that
12862
+ * reads the prototype chain would let a constructed bag corroborate a pairing
12863
+ * it does not actually match.
12864
+ *
12865
+ * `JSON.stringify` was the obvious alternative and is wrong twice over: it is
12866
+ * key-ORDER sensitive (an AWS readback routinely reorders object keys, which
12867
+ * says nothing about the values) and it silently drops `undefined`, so
12868
+ * `{A: undefined}` and `{}` would compare equal while their key counts differ.
12869
+ */
12870
+ function deepEqualJsonValue(a, b) {
12871
+ if (a === b) return true;
12872
+ if (Array.isArray(a)) {
12873
+ if (!Array.isArray(b) || a.length !== b.length) return false;
12874
+ return a.every((item, i) => deepEqualJsonValue(item, b[i]));
12875
+ }
12876
+ if (isPlainObject$2(a)) {
12877
+ if (!isPlainObject$2(b)) return false;
12878
+ if (!hasPlainPrototype(a) || !hasPlainPrototype(b)) return false;
12879
+ const keys = Object.keys(a);
12880
+ if (keys.length !== Object.keys(b).length) return false;
12881
+ return keys.every((k) => Object.hasOwn(b, k) && deepEqualJsonValue(a[k], b[k]));
12882
+ }
12883
+ return false;
12884
+ }
12885
+ /**
12886
+ * Is an equal SOURCE value at an anchor position actually EVIDENCE that the two
12887
+ * containers describe the same element?
12888
+ *
12889
+ * A NON-EMPTY STRING is, and deliberately nothing else is. This is the same bar
12890
+ * {@link isUniquelyKeyedBy} already applies to an identity field, and for the
12891
+ * same reason: `''` is not a distinguishing value (it is also why the value
12892
+ * scan refuses it as a needle), and a non-string carries so few inhabitants
12893
+ * that equality is nearly free -- `{Name: 1, Value: <literal>}` and `{Name: 1,
12894
+ * Value: <expression>}` agree on `Name` whether or not they are the same entry,
12895
+ * so pairing on it would copy a secret reference onto an unrelated literal.
12896
+ * Both shapes are pinned in `secret-redaction-array-identity.test.ts` on the
12897
+ * IDENTITY arm and again in `secret-redaction-anchor-pairing.test.ts` on this
12898
+ * one; an anchor that accepted them would reopen from the positional side
12899
+ * exactly what that arm refuses.
12900
+ *
12901
+ * Containers count when something inside them does, so a nested literal object
12902
+ * can anchor a pairing its own level cannot. That recursion is also why this
12903
+ * predicate ALONE is not enough, and the review that measured it is worth
12904
+ * recording: `AWS::AmazonMQ::Broker.Users` renders `Groups: ['admin']`
12905
+ * identically on every element, which is distinguishing by this test and yet
12906
+ * tells two users APART not at all. Distinguishing is a property of ONE value;
12907
+ * telling elements apart is a property of the WHOLE array, and
12908
+ * {@link unkeyedArrayPairsByAnchors} is where the second one is enforced.
12909
+ *
12910
+ * This is a REFINEMENT of the formulation recorded on issue #2012, which said
12911
+ * only "every position whose SOURCE carries no dynamic reference is deep-equal
12912
+ * on both sides". Taken literally that admits a pairing corroborated ONLY by
12913
+ * `Name: ''` or `Name: 1`, which is measurably wrong: the counterexample the
12914
+ * issue states (`{Name:'', Value:'lit'}` against `{Name:'db', Value:<expr>}`)
12915
+ * has DIFFERING names and refuses on inequality alone, but the fence actually
12916
+ * in the tree carries `Name: ''` on BOTH sides, where equality holds and only
12917
+ * this predicate stands between an unrelated literal and a false redaction.
12918
+ */
12919
+ function isDistinguishingAnchor(value) {
12920
+ if (typeof value === "string") return value !== "";
12921
+ if (Array.isArray(value)) return value.some(isDistinguishingAnchor);
12922
+ if (isPlainObject$2(value)) return Object.values(value).some(isDistinguishingAnchor);
12923
+ return false;
12924
+ }
12925
+ /**
12926
+ * Stands in for a reference-bearing leaf inside an {@link anchorSignature}.
12927
+ *
12928
+ * Written UNQUOTED while `JSON.stringify` quotes every real string, so no
12929
+ * literal can spell it and collide with a masked reference.
12930
+ */
12931
+ const ANCHOR_REFERENCE_MASK = "<ref>";
12932
+ /**
12933
+ * The part of a SOURCE element the anchors can actually see: the element with
12934
+ * every reference-bearing leaf masked, serialized canonically.
12935
+ *
12936
+ * Two elements with the same signature are INDISTINGUISHABLE to this pass --
12937
+ * the anchors say the same thing about both -- so a permutation swapping them
12938
+ * preserves every anchor and the alignment is not determined. That is the
12939
+ * property {@link isUniquelyKeyedBy} enforces for an identity FIELD, restated
12940
+ * for a whole projection instead of a single key.
12941
+ *
12942
+ * The signature is ORDER-INSENSITIVE in both directions -- object keys AND list
12943
+ * elements are sorted -- and that is not cosmetic normalisation. Rule 3's
12944
+ * question is "could AWS hand these two elements back SWAPPED without the swap
12945
+ * being visible", so the projection has to quotient by everything AWS may
12946
+ * itself reorder. Outer list order is the gate's own subject; order WITHIN an
12947
+ * anchor's list is this function's, for exactly the reason `descendArrays:
12948
+ * false` exists at all. A first cut sorted only the keys, and the security
12949
+ * review measured the hole on `AWS::AmazonMQ::Broker.Users`: two users whose
12950
+ * `Groups` were `['admin','ops']` and `['ops','admin']` signed DIFFERENTLY, so
12951
+ * rule 3 passed while the anchors still deep-equalled position for position,
12952
+ * and a reordered `DescribeBroker` put the admin's `Username` / `Password`
12953
+ * expressions at the app user's index. Byte-identical anchor content was being
12954
+ * assumed -- the order assumption this module refuses everywhere else.
12955
+ *
12956
+ * Sorting FAILS CLOSED, which is why it is the right shape of fix: it can only
12957
+ * make two signatures COLLIDE that previously differed, never the reverse, so
12958
+ * its only possible effect is an extra refusal. A missed closure, never a leak.
12959
+ *
12960
+ * The asymmetry with {@link deepEqualJsonValue} is deliberate and must survive
12961
+ * a reader who notices it. That predicate stays order-SENSITIVE on lists
12962
+ * because rule 1 asks a different question -- "did AWS return THIS position
12963
+ * unchanged" -- and a reordered list is a changed position. Making rule 1
12964
+ * order-blind too would weaken the corroboration rather than align it; the case
12965
+ * pinning that is `REFUSES an anchor list REORDERED in place` in
12966
+ * `secret-redaction-anchor-pairing.test.ts`.
12967
+ */
12968
+ function anchorSignature(source) {
12969
+ if (isDynamicReferenceString(source)) return ANCHOR_REFERENCE_MASK;
12970
+ if (Array.isArray(source)) return `[${source.map(anchorSignature).sort().join(",")}]`;
12971
+ if (isPlainObject$2(source)) return `{${Object.keys(source).sort().map((k) => `${JSON.stringify(k)}:${anchorSignature(source[k])}`).join(",")}}`;
12972
+ return JSON.stringify(source) ?? "undefined";
12973
+ }
12974
+ /**
12975
+ * ANCHOR PAIRING (issue #2012): do these two containers corroborate each other
12976
+ * position by position?
12977
+ *
12978
+ * Two conditions, both required:
12979
+ *
12980
+ * 1. the key sets (objects) or index counts (arrays) match, and
12981
+ * 2. every position whose SOURCE carries no dynamic reference is deep-equal on
12982
+ * both sides -- the *anchors*.
12983
+ *
12984
+ * Anchors are what make the pairing EVIDENCE rather than a guess: a position
12985
+ * AWS did not rewrite proves the two containers describe the same element. It
12986
+ * answers the `descendArrays: false` objection on its own terms the way keying
12987
+ * does -- a REORDERED list normally puts a different element under each index,
12988
+ * so its anchors stop matching and the whole array is refused.
12989
+ *
12990
+ * "Normally" is doing real work in that sentence, and an earlier revision of it
12991
+ * did not have the word. A reorder is INVISIBLE to the anchors when the
12992
+ * elements it swaps look the same to them, which is the whole subject of
12993
+ * {@link unkeyedArrayPairsByAnchors}. This function answers only "does position
12994
+ * i corroborate position i"; whether the array as a whole may be walked at all
12995
+ * is decided there, and nothing here is sufficient on its own.
12996
+ *
12997
+ * What it deliberately CANNOT buy is baseline content. Every substitution the
12998
+ * caller then makes is a STRING leaf at a position the bag already has, so a
12999
+ * corroborated pairing never adds a key, adds an element, or writes a scalar
13000
+ * over a container. The principle this module is built on -- **redaction may
13001
+ * not buy itself a fabricated baseline** -- is preserved structurally rather
13002
+ * than by a special case, which is what the first attempt at these rows (taking
13003
+ * the SOURCE array wholesale) failed to do.
13004
+ *
13005
+ * The cost, stated rather than discovered: one deep compare per candidate
13006
+ * position, and a yield that drops to ZERO as soon as AWS normalises any
13007
+ * sibling field in the same container. That is common, so this closes a SUBSET
13008
+ * of the shapes issue #2012 lists rather than all of them, and the residual
13009
+ * stays a refusal -- which is the correct direction to be wrong in here.
13010
+ */
13011
+ function anchorsCorroboratePairing(bag, source, anchors) {
13012
+ if (!subtreeHasDynamicReference(source)) {
13013
+ if (!deepEqualJsonValue(bag, source)) return false;
13014
+ if (isDistinguishingAnchor(source)) anchors.distinguishing += 1;
13015
+ return true;
13016
+ }
13017
+ if (isDynamicReferenceString(source)) return typeof bag === "string";
13018
+ if (isPlainObject$2(source)) {
13019
+ if (!isPlainObject$2(bag)) return false;
13020
+ const sourceKeys = Object.keys(source);
13021
+ if (sourceKeys.length !== Object.keys(bag).length) return false;
13022
+ return sourceKeys.every((k) => Object.hasOwn(bag, k) && anchorsCorroboratePairing(bag[k], source[k], anchors));
13023
+ }
13024
+ if (Array.isArray(source)) {
13025
+ if (!Array.isArray(bag) || bag.length !== source.length) return false;
13026
+ return source.every((item, i) => anchorsCorroboratePairing(bag[i], item, anchors));
13027
+ }
13028
+ return false;
13029
+ }
13030
+ /**
13031
+ * May this UNKEYED array be walked positionally? The gate the anchor relaxation
13032
+ * actually rests on (issue #2012 review).
13033
+ *
13034
+ * {@link anchorsCorroboratePairing} answers per POSITION. Asking it once for
13035
+ * the whole array and requiring one distinguishing anchor ANYWHERE in the
13036
+ * result -- which an earlier revision did -- is unsound in two INDEPENDENT
13037
+ * ways, both measured by review against real shapes rather than reasoned about:
13038
+ *
13039
+ * - **Evidence for one element was credited to another.** `[{Name:'db',
13040
+ * Value:<exprA>}, {Name:'', Value:<exprB>}]` has a distinguishing anchor at
13041
+ * index 0 and NONE at index 1, and the array-wide counter licensed both -- so
13042
+ * an unrelated literal at index 1 took `<exprB>`. That is precisely the false
13043
+ * redaction `isDistinguishingAnchor` exists to prevent, arriving through the
13044
+ * counter's SCOPE instead of through its definition.
13045
+ * - **Equal anchors cannot detect a reorder.** `['--pw', <exprA>, '--pw',
13046
+ * <exprB>]` against a readback holding the two values swapped matches every
13047
+ * anchor at every index, because both anchors are `'--pw'` -- so each
13048
+ * position was pinned to the OTHER secret's expression.
13049
+ * `AWS::AmazonMQ::Broker.Users` is the shape that makes this real rather than
13050
+ * contrived: no `Name`/`Key`, both `Username` and `Password` rendered through
13051
+ * `secretValueFromJson`, and `Groups: ['admin']` equal on every element, so a
13052
+ * `DescribeBroker` returning the users in the other order records the ADMIN
13053
+ * credential's reference at the app user's position -- which `cdkd drift
13054
+ * --revert` then pushes to the live broker.
13055
+ *
13056
+ * So the gate is:
13057
+ *
13058
+ * 1. **Every position corroborates**, with the counter scoped PER top-level
13059
+ * element rather than shared across the array.
13060
+ * 2. **Every reference-bearing element carries its own evidence.** A CONTAINER
13061
+ * must hold a distinguishing anchor INSIDE it: it has an interior where an
13062
+ * identity could live, so the absence of one is meaningful. A BARE reference
13063
+ * leaf has no interior, so absence says nothing about it and the only
13064
+ * evidence available is the FRAME -- the array's non-reference-bearing
13065
+ * elements, which must then supply a distinguishing anchor between them.
13066
+ * That distinction is exactly what separates `['--pw', <expr>, '--verbose']`
13067
+ * (CLOSES: the literal flags pin the one free slot) from `[{V:'us-east-1'},
13068
+ * {V:<expr>}]` (REFUSES: the second element could hold anything, and
13069
+ * overwriting it would erase a genuine out-of-band change from the drift
13070
+ * baseline, so `cdkd drift` reports clean and `--revert` never sees it).
13071
+ * 3. **Reference-bearing elements are pairwise DISTINGUISHABLE**, by
13072
+ * {@link anchorSignature}. Two elements the anchors describe identically
13073
+ * admit a permutation that preserves every anchor, so the alignment is not
13074
+ * determined and no amount of per-position equality makes it so.
13075
+ *
13076
+ * Checking uniqueness on the SOURCE side alone is sufficient, and the argument
13077
+ * is worth stating because the bag side looks like it needs checking too: rule
13078
+ * 1 has already established that the bag matches the source at every anchor
13079
+ * position, so the two projections are equal element-wise. If some permutation
13080
+ * other than the identity also satisfied the anchors, two SOURCE elements would
13081
+ * have to share a signature -- which rule 3 excludes. This is the same
13082
+ * multiset-correctness argument `isUniquelyKeyedBy` makes for a single field.
13083
+ *
13084
+ * NESTED arrays are not re-checked here, and do not need to be: the caller
13085
+ * recurses through {@link refuseUncertifiedReadbackPositions}, which re-enters
13086
+ * its own array arm for every nested list and consults this gate again with
13087
+ * that list's own elements. A nested array whose elements are indistinguishable
13088
+ * is therefore refused on its own terms while its parent may still pair.
13089
+ */
13090
+ function unkeyedArrayPairsByAnchors(bag, source) {
13091
+ if (bag.length !== source.length) return false;
13092
+ const distinguishingPerElement = [];
13093
+ for (const [i, item] of source.entries()) {
13094
+ const anchors = { distinguishing: 0 };
13095
+ if (!anchorsCorroboratePairing(bag[i], item, anchors)) return false;
13096
+ distinguishingPerElement.push(anchors.distinguishing);
13097
+ }
13098
+ const frameDistinguishing = source.reduce((total, item, i) => subtreeHasDynamicReference(item) ? total : total + distinguishingPerElement[i], 0);
13099
+ const signatures = /* @__PURE__ */ new Set();
13100
+ for (const [i, item] of source.entries()) {
13101
+ if (!subtreeHasDynamicReference(item)) continue;
13102
+ const signature = anchorSignature(item);
13103
+ if (signatures.has(signature)) return false;
13104
+ signatures.add(signature);
13105
+ if (distinguishingPerElement[i] > 0) continue;
13106
+ if (!isDynamicReferenceString(item) || frameDistinguishing === 0) return false;
13107
+ }
13108
+ return true;
13109
+ }
13110
+ /**
12825
13111
  * Refuse to persist a readback leaf the path pass could not CERTIFY, at any
12826
13112
  * position the STATE source proves is secret-bearing (issue #1926 review).
12827
13113
  *
@@ -12841,8 +13127,9 @@ function mixedLeafMayCarryPublicReference(source, secrets) {
12841
13127
  * ----------------------------------------------- ------------ ---------------
12842
13128
  * `postgres://u:{{resolve:...}}@h` (MIXED string) LEAK take source
12843
13129
  * ...the same MIXED leaf inside a PAIRED element LEAK take source
12844
- * `['--pw', '{{resolve:...}}']` (no identity key) LEAK LEAK (#2012)
12845
- * `[{Field, Val: '{{resolve:...}}'}]` (no `Name`) LEAK LEAK (#2012)
13130
+ * `['--pw', '{{resolve:...}}']` (no identity key) LEAK take source*
13131
+ * `[{Field, Val: '{{resolve:...}}'}]` (no `Name`) LEAK take source*
13132
+ * ...either of those, but REORDERED / normalised LEAK LEAK (#2012)
12846
13133
  * an UNPAIRED element beside a paired one LEAK LEAK (#2012)
12847
13134
  * an observed KEY the source does not carry LEAK LEAK (#2012)
12848
13135
  * whole `{{resolve:...}}` token ok ok
@@ -12864,7 +13151,27 @@ function mixedLeafMayCarryPublicReference(source, secrets) {
12864
13151
  * not a WHOLE token. Everything it takes is the record's own value at the
12865
13152
  * record's own path.
12866
13153
  *
12867
- * The four residual rows are one root cause, not four: no needle and no
13154
+ * The starred rows are the two ANCHOR PAIRING closed for issue #2012, and the
13155
+ * star is load-bearing: they close only when the pairing is CORROBORATED. Four
13156
+ * conditions, all required — the index counts match; every position the source
13157
+ * does not spell as a reference is deep-equal on both sides; every
13158
+ * reference-bearing ELEMENT carries its own distinguishing anchor, or, being a
13159
+ * bare reference leaf with no interior to carry one, leans on the array's
13160
+ * literal FRAME; and no two reference-bearing elements look alike to the
13161
+ * anchors. That is why the row beneath them exists. As soon as AWS reorders the
13162
+ * list or normalises any sibling field, the anchors stop matching and the same
13163
+ * two shapes refuse again, so the closure is a SUBSET of each row rather than
13164
+ * the whole of it.
13165
+ *
13166
+ * An earlier revision of this paragraph stated only the first two conditions
13167
+ * plus "at least one of them distinguishing", which was the gate BEFORE the
13168
+ * #2012 review — under it `['--pw', <exprA>, '--pw', <exprB>]` closes and
13169
+ * misattributes, so the text documented the defect as the design. See
13170
+ * {@link unkeyedArrayPairsByAnchors}, which is where all four live;
13171
+ * {@link anchorsCorroboratePairing} answers only one of them and its own doc
13172
+ * says nothing in it is sufficient alone.
13173
+ *
13174
+ * The residual rows are one root cause, not several: no needle and no
12868
13175
  * position, so nothing distinguishes a resolved secret from an ordinary
12869
13176
  * literal. They are NOT closed by taking the source subtree, which an earlier
12870
13177
  * revision did and the issue #1915 fences correctly rejected — measured, it
@@ -12872,7 +13179,9 @@ function mixedLeafMayCarryPublicReference(source, secrets) {
12872
13179
  * turned an AWS-reported `[{Value:'x'}]` into `[{Name:'db', Value:<expr>}]`,
12873
13180
  * fabricating drift-baseline content AWS never reported that `cdkd drift
12874
13181
  * --revert` then pushes to the live resource. Redaction may not buy itself a
12875
- * fabricated baseline.
13182
+ * fabricated baseline — which is also the bar anchor pairing had to clear, and
13183
+ * clears structurally: it only ever licenses a walk of positions the BAG
13184
+ * already has, so it can add no key, no element and no scalar-over-container.
12876
13185
  *
12877
13186
  * The MIXED row is the shape this module itself calls DOMINANT for CDK — an
12878
13187
  * `Fn::Join` around `secret.secretValueFromJson(...)`.
@@ -12907,7 +13216,10 @@ function refuseUncertifiedReadbackPositions(bag, source, secrets) {
12907
13216
  }
12908
13217
  if (Array.isArray(bag) && Array.isArray(source)) {
12909
13218
  const key = identityKeyFor(bag, source);
12910
- if (key === void 0) return bag;
13219
+ if (key === void 0) {
13220
+ if (!unkeyedArrayPairsByAnchors(bag, source)) return bag;
13221
+ return bag.map((item, i) => refuseUncertifiedReadbackPositions(item, source[i], secrets));
13222
+ }
12911
13223
  const sourceByIdentity = /* @__PURE__ */ new Map();
12912
13224
  for (const item of source) sourceByIdentity.set(item[key], item);
12913
13225
  return bag.map((item) => {
@@ -18844,7 +19156,7 @@ var CloudControlProvider = class {
18844
19156
  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);
18845
19157
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
18846
19158
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
18847
- const { ASGProvider } = await import("./asg-provider-fy2cEq7m.js").then((n) => n.n);
19159
+ const { ASGProvider } = await import("./asg-provider-CLnf8ct8.js").then((n) => n.n);
18848
19160
  return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
18849
19161
  }
18850
19162
  const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
@@ -20506,6 +20818,94 @@ function hasHandlerLogOutput(logTail) {
20506
20818
  return logTail.split("\n").some((line) => line.trim().length > 0 && !CR_LOG_TAIL_BOILERPLATE.test(line.trimStart()));
20507
20819
  }
20508
20820
  /**
20821
+ * Parse a response body without trusting it. The body is written by the
20822
+ * customer's Lambda handler through a pre-signed URL, so it is UNTRUSTED
20823
+ * input: it may be truncated, may be a JSON scalar, or may be `null`. Every
20824
+ * one of those must be a normal "keep polling" outcome rather than a throw.
20825
+ */
20826
+ function parseCfnResponseBody(body) {
20827
+ let value;
20828
+ try {
20829
+ value = JSON.parse(body);
20830
+ } catch {
20831
+ return { kind: "unparseable" };
20832
+ }
20833
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return { kind: "non-object" };
20834
+ return {
20835
+ kind: "envelope",
20836
+ response: value
20837
+ };
20838
+ }
20839
+ /**
20840
+ * Every field of the response is HANDLER-CONTROLLED, so each one is made safe
20841
+ * to render before it reaches a log line, and capped.
20842
+ *
20843
+ * Both halves are regressions this function introduced and a review caught.
20844
+ * The line it replaced printed `body.substring(0, 200)` -- raw WIRE json, where
20845
+ * the encoder had already escaped control characters as `\u001b`, and which was
20846
+ * capped at 200 characters by construction. Parsing first UNDOES the escaping:
20847
+ * an ESC and a newline reach the terminal as real bytes, so a handler (or
20848
+ * anyone holding the pre-signed response URL) could clear the screen and print
20849
+ * a forged `ERROR [cdkd]` line into a CI transcript. Measured: a
20850
+ * `PhysicalResourceId` carrying `ESC[2J` plus a newline rendered both. And
20851
+ * dropping the substring removed the bound -- a 5000-char id with 300 `Data`
20852
+ * keys rendered a 19,714-character line, re-emitted on EVERY poll.
20853
+ *
20854
+ * `displaySafe` is this repo's one answer to the first (issue
20855
+ * https://github.com/go-to-k/cdkd/issues/2170); `state.ts` already applies the
20856
+ * same treatment to this very value when it prints a state record.
20857
+ */
20858
+ function capForLog(value) {
20859
+ const safe = displaySafe(value);
20860
+ const capped = safe.length > DESCRIBE_MAX_FIELD_CHARS ? `${safe.slice(0, DESCRIBE_MAX_FIELD_CHARS)}...(${safe.length} chars)` : safe;
20861
+ return JSON.stringify(capped);
20862
+ }
20863
+ /** Per-field cap for the poll log line. */
20864
+ const DESCRIBE_MAX_FIELD_CHARS = 200;
20865
+ /** Whole-line clamp, applied after the per-field caps. */
20866
+ const DESCRIBE_MAX_LINE_CHARS = 1e3;
20867
+ /** How many `Data` key names the poll log line names before counting the rest. */
20868
+ const DESCRIBE_MAX_DATA_KEYS = 20;
20869
+ /**
20870
+ * Render a NON-SENSITIVE one-line summary of a custom-resource response body
20871
+ * for the poll's debug log (issue #2250).
20872
+ *
20873
+ * The body is the CloudFormation custom-resource response document, and its
20874
+ * `Data` field is the documented place a handler returns a GENERATED VALUE —
20875
+ * including a generated secret. The previous log line emitted
20876
+ * `body.substring(0, 200)`, which put those values on the terminal (and, in
20877
+ * CI, into the retained build log) on every poll under `--verbose`.
20878
+ *
20879
+ * What survives here is everything the line was actually useful for: WHICH
20880
+ * resource answered (the caller adds the logical id), WHETHER it succeeded
20881
+ * (`Status`), what identity it claimed (`PhysicalResourceId` — already
20882
+ * persisted to state.json, so not a new channel), and WHICH keys came back
20883
+ * (`Object.keys(Data)`). The `Data` VALUES never appear, and neither does
20884
+ * `Reason`, which is free-form handler text that can quote them.
20885
+ *
20886
+ * For a body that is not a usable envelope, only its LENGTH is reported —
20887
+ * never its bytes. That keeps the diagnostic for the case it matters most in
20888
+ * (a handler writing a malformed response) without turning the fallback into
20889
+ * the same prefix echo through another door.
20890
+ */
20891
+ function describeCfnResponseBody(body, parsed) {
20892
+ if (parsed.kind !== "envelope") return `${parsed.kind === "unparseable" ? "unparseable body" : "JSON body is not an object"} (${body.length} chars)`;
20893
+ const envelope = parsed.response;
20894
+ const status = typeof envelope["Status"] === "string" ? envelope["Status"] : "<absent>";
20895
+ const physicalId = typeof envelope["PhysicalResourceId"] === "string" ? envelope["PhysicalResourceId"] : "<absent>";
20896
+ const data = envelope["Data"];
20897
+ let dataPart;
20898
+ if (data === void 0) dataPart = "Data absent";
20899
+ else if (typeof data === "object" && data !== null && !Array.isArray(data)) {
20900
+ const keys = Object.keys(data);
20901
+ const shown = keys.slice(0, DESCRIBE_MAX_DATA_KEYS).map((k) => capForLog(k));
20902
+ const omitted = keys.length - shown.length;
20903
+ dataPart = `Data keys [${shown.join(", ")}${omitted > 0 ? `, +${omitted} more` : ""}]`;
20904
+ } else dataPart = "Data not an object";
20905
+ const line = `Status=${capForLog(status)} PhysicalResourceId=${capForLog(physicalId)} ${dataPart}`;
20906
+ return line.length > DESCRIBE_MAX_LINE_CHARS ? `${line.slice(0, DESCRIBE_MAX_LINE_CHARS)}...(${line.length} chars total)` : line;
20907
+ }
20908
+ /**
20509
20909
  * Custom Resource Provider
20510
20910
  *
20511
20911
  * Implements Lambda-backed custom resources by invoking the Lambda function
@@ -21188,7 +21588,7 @@ var CustomResourceProvider = class CustomResourceProvider {
21188
21588
  }
21189
21589
  /** Truncate a CR FAILED reason for log readability. */
21190
21590
  truncateReason(reason, max = 200) {
21191
- const r = reason ?? "Unknown reason";
21591
+ const r = displaySafe(reason ?? "Unknown reason");
21192
21592
  return r.length > max ? `${r.slice(0, max)}...` : r;
21193
21593
  }
21194
21594
  /**
@@ -21503,16 +21903,15 @@ var CustomResourceProvider = class CustomResourceProvider {
21503
21903
  Key: responseKey
21504
21904
  }))).Body?.transformToString();
21505
21905
  if (body && body.length > 0) {
21506
- this.logger.debug(`Got S3 response for ${logicalId}: ${body.substring(0, 200)}`);
21507
- try {
21508
- const cfnResponse = JSON.parse(body);
21906
+ const parsed = parseCfnResponseBody(body);
21907
+ this.logger.debug(`Got S3 response for ${logicalId}: ${describeCfnResponseBody(body, parsed)}`);
21908
+ if (parsed.kind === "envelope") {
21909
+ const cfnResponse = parsed.response;
21509
21910
  if (cfnResponse.Status === "SUCCESS" || cfnResponse.Status === "FAILED") {
21510
21911
  await this.cleanupResponseObject(responseKey);
21511
21912
  return cfnResponse;
21512
21913
  }
21513
- } catch {
21514
- this.logger.debug(`S3 response not yet valid JSON for ${logicalId}, retrying...`);
21515
- }
21914
+ } else this.logger.debug(`S3 response not yet valid JSON for ${logicalId}, retrying...`);
21516
21915
  }
21517
21916
  } catch (error) {
21518
21917
  const err = error;
@@ -30544,4 +30943,4 @@ var DeployEngine = class {
30544
30943
 
30545
30944
  //#endregion
30546
30945
  export { maskerOrIdentity as $, findLargeInlineResources as $n, TemplateParser as $t, renderStatefulReason as A, formatDockerLoginError as An, StackHasActiveImportsError as Ar, STATE_SOURCED_READBACK_RULES as At, exportAliasCollisionScrubWarning as B, getLegacyStateBucketName as Bn, isThrottlingError as Br, classifyReplaySecretRegion as Bt, isFinalSnapshotError as C, parseBootstrapMarker as Cn, LockError as Cr, configStringRefusal as Ct, extractDeploymentEventError as D, buildDenyExternalAccessPolicy as Dn, ProvisioningError as Dr, requireConfigObject as Dt, makeCanonicalizePropertiesFn as E, validateContainerRepoName as En, PartialFailureError as Er, requireConfigArray as Et, green as F, AssetManifestLoader as Fn, isCdkdError as Fr, isSingleDynamicReferenceToken as Ft, IAMRoleProvider as G, resolveStateBucketWithDefault as Gn, s3BucketRegionalDomainName as Gt, secretBearingStateKeyWarning as H, resolveAutoAssetStorage as Hn, __exportAll as Hr, s3BucketArn as Ht, red as I, getDockerImageBySourceHash as In, normalizeAwsError as Ir, maskSecretsInError as It, ProviderRegistry as J, stateBucketExistenceConfirmed as Jn, DiffCalculator as Jt, collectInlinePolicyNamesManagedBySiblings as K, resolveStateBucketWithDefaultAndSource as Kn, s3BucketWebsiteUrl as Kt, yellow as L, Synthesizer as Ln, withErrorHandling as Lr, maskSecretsInText as Lt, bold as M, partitionSensitiveEnv as Mn, StateError as Mr, createSecretMasker as Mt, cyan as N, runDockerForeground as Nn, SynthesisError as Nr, dynamicReferenceTokens as Nt, MULTI_REGION_RECREATE_BLOCKED_TYPES as O, buildDockerImage as On, ResourceTimeoutError as Or, requireConfigString as Ot, gray as P, runDockerStreaming as Pn, formatError as Pr, errorCauseChain as Pt, maskDeep as Q, MIGRATE_TMP_PREFIX as Qn, DagBuilder as Qt, collectDeclaredOutputNames as R, synthesisStatusMessage as Rn, isMarkedNonRetryable as Rr, redactSecretsForState as Rt, createPreDeleteFinalSnapshot as S, isCrossRegionRedirect as Sn, LocalStartServiceError as Sr, configBooleanRefusal as St, unsupportedFinalSnapshotError as T, validateAssetBucketName as Tn, NestedStackChildDirectDestroyError as Tr, replayWarn as Tt, stateKeySecretExposure as U, resolveCaptureObservedState as Un, s3BucketDomainName as Ut, isExportAliasCollision as V, resolveApp as Vn, markNonRetryable as Vr, producerRegionsFromState as Vt, getCurrentResourceSecrets as W, resolveSkipPrefix as Wn, s3BucketDualStackDomainName as Wt, findSilentDropProperties as X, CFN_TEMPLATE_BODY_LIMIT as Xn, describeTypeWithThrottleRetry as Xt, findActionableSilentDrops as Y, warnDeprecatedNoPrefixCliFlag as Yn, INTRINSIC_KEYS as Yt, createMaskedRetryLogger as Z, CFN_TEMPLATE_URL_LIMIT as Zn, withRetry as Zt, computeImplicitDeleteEdges as _, AssetModeResolver as _n, DependencyError as _r, WAFv2WebACLProvider as _t, DeploymentEventsStore as a, importableOutputKeys as an, AssemblyReader as ar, isInterruptedWaitError as at, buildFinalSnapshotIdentifier as b, ensureAssetStorage as bn, LocalInvokeBuildError as br, assertRegionMatch as bt, replayFailedOperations as c, AssetPublisher as cn, resolveBucketRegion as cr, slowCcOperationTimeoutMs as ct, updatePartialReason as d, buildAssetRedirectMap as dn, resetAwsClients as dr, IntrinsicFunctionResolver as dt, LockManager as en, uploadCfnTemplate as er, CUSTOM_RESOURCE_RESPONSE_PREFIX as et, UNSPECIFIED_SKIP_REASON as f, createAssetRedirectResolver as fn, setAwsClients as fr, carriesDynamicReference as ft, IMPLICIT_DELETE_DEPENDENCIES as g, stripControlChars as gn, CrossAccountSecretRefusalError as gr, refStateLookupFromResource as gt, maskingRetryLogger as h, escapeRegExp$1 as hn, ConfigError as hr, parameterTypeMayLoseSecretIdentity as ht, DeploymentEventsReader as i, exportNamesCarriedFrom as in, derivePartitionAndUrlSuffix as ir, interruptWatchListenerCount as it, formatResourceLine as j, getDockerCmd as jn, StackTerminationProtectionError as jr, TEMPLATE_SOURCED_RULES as jt, isStatefulRecreateTargetSync as k, dockerSpawnEnvWithSensitive as kn, ResourceUpdateNotSupportedError as kr, STATE_SOURCED_CROSS_GENERATION_RULES as kt, replayRollback as l, stringifyValue as ln, AwsClients as lr, disableInstanceApiTermination as lt, withResourceDeadline as m, rewriteTemplateAssetReferences as mn, CdkdError as mr, getAccountInfo as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, S3StateBackend as nn, PARTITION_TABLE as nr, beginCommandInterruptScope as nt, planFailedOps as o, importableOutputs as on, processStackMessages as or, startInterruptWatch as ot, deleteSkipReason as p, loadPublishableAssetManifest as pn, AssetError as pr, cfnRefValueFromPhysicalId as pt, clearOnUpdateRemoval as q, resolveUseCdkBootstrapAssets as qn, applyRoleArnIfSet as qt, DeployEngine as r, rebuildClientForBucketRegion as rn, canonicalizeRegion as rr, endCommandInterruptScope as rt, planRollback as s, shouldRetainResource as sn, clearBucketRegionCache as sr, CloudControlProvider as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, displaySafe as tn, expectedOwnerParam as tr, DEFAULT_STATE_PREFIX as tt, updatePartialMessage as u, WorkGraph as un, getAwsClients as ur, isTerminationProtectionPropagationError as ut, ATOMIC_FINAL_SNAPSHOT_TYPES as v, BOOTSTRAP_MARKER_PREFIX as vn, DeployCancelledError as vr, normalizeAwsTagsToCfn as vt, refusesFinalSnapshot as w, readBootstrapMarkerBody as wn, MissingCdkCliError as wr, readConfigString as wt, ccRoutedFinalSnapshotError as x, getBootstrapMarkerKey as xn, LocalMigrateError as xr, coerceCfnBoolean as xt, PRE_DELETE_SNAPSHOT_TYPES as y, assertAssetBucketRegion as yn, DynamicReferenceRegionAmbiguousError as yr, resolveExplicitPhysicalId as yt, collectPublishedOutputNames as z, getDefaultStateBucketName as zn, isRetryableTransientError as zr, scrubResourceRecord as zt };
30547
- //# sourceMappingURL=deploy-engine-wrIe6cV6.js.map
30946
+ //# sourceMappingURL=deploy-engine-CHLe31Fh.js.map