@go-to-k/cdkd 0.284.7 → 0.284.8

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.
@@ -10604,13 +10604,227 @@ function redactByPath(bag, source, secrets, rules, secretExpressions) {
10604
10604
  if (rules.descendArrays && bag.length === source.length) return bag.map((item, i) => redactByPath(item, source[i], secrets, rules, secretExpressions));
10605
10605
  }
10606
10606
  if (isPlainObject$2(bag) && isPlainObject$2(source)) {
10607
- const out = {};
10607
+ const out = Object.create(null);
10608
10608
  for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? redactByPath(v, source[k], secrets, rules, secretExpressions) : redactSecretsForState(v, secrets);
10609
10609
  return out;
10610
10610
  }
10611
10611
  return redactSecretsForState(bag, secrets);
10612
10612
  }
10613
10613
  /**
10614
+ * Is this rules constant one whose BAG is an AWS readback and whose SOURCE is a
10615
+ * persisted STATE bag?
10616
+ *
10617
+ * Today that is {@link STATE_SOURCED_READBACK_RULES} alone: the path where the
10618
+ * secrets map can be EMPTY by construction (nothing was resolved), so the value
10619
+ * scan has no needles and POSITION is the only mechanism left. Derived from the
10620
+ * flags rather than compared against the constant so a future one with the same
10621
+ * shape is covered automatically. `trustAnyExpression` says the source is a
10622
+ * persisted record (holding no PUBLIC reference, so any `{{resolve:...}}` in it
10623
+ * is by construction a secret); `!descendArrays` says the bag came back from
10624
+ * AWS and may be reordered.
10625
+ *
10626
+ * `sourceIsSameGeneration` is the third conjunct and it is the one that took a
10627
+ * measurement to get right. Without it this also selected
10628
+ * {@link STATE_SOURCED_CROSS_GENERATION_RULES} — `cdkd scrub`'s observed walk,
10629
+ * whose `properties` have ALREADY been repositioned onto TODAY's template — and
10630
+ * taking a source subtree there rewrote a baseline holding the DEPLOYED
10631
+ * `:AWSPREVIOUS` reference onto the template's edited `:AWSCURRENT` one. That
10632
+ * is precisely the issue #1917 hazard, and `cdkd drift --revert` pushes the
10633
+ * baseline to AWS, so it would have applied a reference the stack never
10634
+ * deployed. A refusal may only take a source that is the same generation as the
10635
+ * bag beside it.
10636
+ *
10637
+ * A TEMPLATE-sourced caller is deliberately excluded: its source can carry a
10638
+ * public `ssm:` reference whose resolved value must STAY resolved (issue
10639
+ * #1901), and it always has a populated map, so the value scan already covers
10640
+ * the shapes below. So is the rollback replay
10641
+ * ({@link STATE_DERIVED_RULES}) — full map, and its bag descends positionally
10642
+ * because it was produced by resolving the source.
10643
+ */
10644
+ function isReadbackProjectedFromState(rules) {
10645
+ return rules.trustAnyExpression && !rules.descendArrays && rules.sourceIsSameGeneration;
10646
+ }
10647
+ /**
10648
+ * Does this subtree carry a dynamic reference anywhere?
10649
+ *
10650
+ * A BOOLEAN, not the occurrence COUNTS an earlier revision collected. The
10651
+ * counts existed to decide whether a bag "covered" every reference its source
10652
+ * carried, which was the vouching rule for taking a source array wholesale —
10653
+ * and that rule is gone (see the array arm), so counting would be a
10654
+ * measurement nothing reads.
10655
+ */
10656
+ function subtreeHasDynamicReference(value) {
10657
+ if (isDynamicReferenceString(value)) return true;
10658
+ if (Array.isArray(value)) return value.some(subtreeHasDynamicReference);
10659
+ if (isPlainObject$2(value)) return Object.values(value).some(subtreeHasDynamicReference);
10660
+ return false;
10661
+ }
10662
+ /** Every complete `{{resolve:...}}` token inside a string. */
10663
+ function dynamicReferenceTokens(value) {
10664
+ return value.match(/\{\{resolve:[^{}]*\}\}/g) ?? [];
10665
+ }
10666
+ /**
10667
+ * Does this MIXED leaf embed a reference that may be PUBLIC config?
10668
+ *
10669
+ * A plain `{{resolve:ssm:...}}` is classified by the parameter's TYPE, not by
10670
+ * its spelling (issue #1901): a `String` / `StringList` parameter is public and
10671
+ * is legitimately persisted RESOLVED. Substituting the expression over it gives
10672
+ * the drift baseline a value AWS does not hold, which is phantom drift on
10673
+ * ordinary config — and `--revert` then pushes the literal expression.
10674
+ *
10675
+ * `trustAnyExpression` is what would otherwise wave this through, and its
10676
+ * premise ("a persisted STATE bag holds no public expression") is documented as
10677
+ * FALSE in one place: `cdkd import`'s warn path can leave one there. The
10678
+ * whole-token arm accepts that risk knowingly and `cdkd drift --accept`
10679
+ * re-checks its write; the MIXED arm added later has no such re-check, so it
10680
+ * declines instead.
10681
+ *
10682
+ * A reference the resolver RECORDED as secret is kept: that is the ssm
10683
+ * `SecureString` case, where the verdict came off the same `GetParameter`
10684
+ * response that carried the value. `{{resolve:ssm-secure:` does not match this
10685
+ * prefix at all (the next character is `-`), so it is never refused here.
10686
+ *
10687
+ * The verdict store only carries signal where something RESOLVED, so this
10688
+ * splits on whether a secrets map exists at all.
10689
+ *
10690
+ * WITH a map, a pass resolved this bag: the engine's change detection walks
10691
+ * every template property with `skipDynamicReferences`, which only flips
10692
+ * `decrypt` on the ssm branch — the `GetParameter` still runs, a definitive
10693
+ * `SecureString` is recorded, and a parameter that comes back public has its
10694
+ * memo RETRACTED. Absence from the store is then real evidence of a public
10695
+ * parameter, and the resolved value is kept.
10696
+ *
10697
+ * WITHOUT one, absence means only that the question was never asked HERE. It
10698
+ * does not mean nothing was resolved: the deploy path resolves every template
10699
+ * property with `skipDynamicReferences`, which records or retracts the
10700
+ * `SecureString` verdict even for an UNCHANGED resource -- that bag simply is
10701
+ * not the one this call receives. The leaf is treated as
10702
+ * secret-bearing and refused. That is not merely the cautious branch, it is the
10703
+ * SAME premise the whole-token arm one level up already acts on: a PUBLIC
10704
+ * `String` / `StringList` reference is persisted RESOLVED (issue #1901), so a
10705
+ * `{{resolve:ssm:` token that SURVIVES in a persisted state bag is a
10706
+ * SecureString by construction. An earlier revision applied a stricter rule to
10707
+ * a MIXED leaf than to a whole token on the identical source, and that
10708
+ * inconsistency is what persisted a decrypted secret.
10709
+ *
10710
+ * ACCEPTED CONSEQUENCE, recorded rather than papered over: on the empty-map
10711
+ * paths a genuinely PUBLIC ssm mixed leaf is now OVER-redacted, so the baseline
10712
+ * no longer matches AWS and `cdkd drift` reports a phantom on it. That is
10713
+ * reachable only through the one documented hole in the premise above — `cdkd
10714
+ * import`'s warn path, which can leave a public expression in state. The trade
10715
+ * is deliberate and asymmetric: under-redaction persists a decrypted secret,
10716
+ * which is a disclosure and is what this lane exists to prevent, while
10717
+ * over-redaction is visible, recoverable and discloses nothing. Closing it
10718
+ * properly needs a real TYPE classification on these paths, which is issue
10719
+ * [#2012](https://github.com/go-to-k/cdkd/issues/2012)'s mechanism; the
10720
+ * over-redaction itself is tracked as issue
10721
+ * [#2036](https://github.com/go-to-k/cdkd/issues/2036).
10722
+ *
10723
+ * `tests/integration/secrets-dynamic-ref` is the end-to-end proof on BOTH
10724
+ * paths, and it is the only place the empty-map defect surfaced: Phase 1g
10725
+ * covers the populated-map deploy and Phase 1f the empty-map command.
10726
+ */
10727
+ function mixedLeafMayCarryPublicReference(source, secrets) {
10728
+ if (secrets.size === 0) return false;
10729
+ return dynamicReferenceTokens(source).some((token) => token.startsWith("{{resolve:ssm:") && !isRecordedSecretExpression(token));
10730
+ }
10731
+ /**
10732
+ * Refuse to persist a readback leaf the path pass could not CERTIFY, at any
10733
+ * position the STATE source proves is secret-bearing (issue #1926 review).
10734
+ *
10735
+ * {@link redactByPath} substitutes only where the source leaf is a WHOLE
10736
+ * `{{resolve:...}}` token. On the paths {@link isReadbackProjectedFromState}
10737
+ * selects the secrets map may be EMPTY, so its value-scan fallback is a no-op,
10738
+ * and four shapes reached `state.json` holding the DECRYPTED value. Measured
10739
+ * against this module before this pass existed — three by `cdkd state
10740
+ * refresh-observed`, and the same three by a plain `cdkd deploy`, whose
10741
+ * `drainObservedCaptures` baseline reaches the persist choke point with exactly
10742
+ * this configuration. `cdkd scrub` is NOT one of them: its observed walk is
10743
+ * CROSS-generation, so {@link isReadbackProjectedFromState} excludes it by
10744
+ * design:
10745
+ *
10746
+ * ```text
10747
+ * source leaf in the STATE record before now
10748
+ * ----------------------------------------------- ------------ ---------------
10749
+ * `postgres://u:{{resolve:...}}@h` (MIXED string) LEAK take source
10750
+ * ...the same MIXED leaf inside a PAIRED element LEAK take source
10751
+ * `['--pw', '{{resolve:...}}']` (no identity key) LEAK LEAK (#2012)
10752
+ * `[{Field, Val: '{{resolve:...}}'}]` (no `Name`) LEAK LEAK (#2012)
10753
+ * an UNPAIRED element beside a paired one LEAK LEAK (#2012)
10754
+ * an observed KEY the source does not carry LEAK LEAK (#2012)
10755
+ * whole `{{resolve:...}}` token ok ok
10756
+ * `Environment[]` keyed by `Name` (issue #1915) ok ok
10757
+ * PUBLIC ssm MIXED leaf, POPULATED map ok ok
10758
+ * PUBLIC ssm MIXED leaf, EMPTY map ok over-redacts
10759
+ * ```
10760
+ *
10761
+ * The last row is the price of the row above it and is tracked as issue
10762
+ * [#2036](https://github.com/go-to-k/cdkd/issues/2036): with no map nothing was
10763
+ * resolved, so nothing distinguishes a public parameter from a `SecureString`
10764
+ * and the leaf is refused. Phantom drift, not a disclosure — see
10765
+ * {@link mixedLeafMayCarryPublicReference} for why that is the right way to be
10766
+ * wrong here.
10767
+ *
10768
+ * What this pass closes is the row POSITION can actually justify: a leaf whose
10769
+ * KEY the source carries, where the source is the same generation and the only
10770
+ * thing the older code lacked was the willingness to substitute a leaf that was
10771
+ * not a WHOLE token. Everything it takes is the record's own value at the
10772
+ * record's own path.
10773
+ *
10774
+ * The four residual rows are one root cause, not four: no needle and no
10775
+ * position, so nothing distinguishes a resolved secret from an ordinary
10776
+ * literal. They are NOT closed by taking the source subtree, which an earlier
10777
+ * revision did and the issue #1915 fences correctly rejected — measured, it
10778
+ * rewrote `{Name:'', Value:'an-unrelated-literal'}` onto the expression and
10779
+ * turned an AWS-reported `[{Value:'x'}]` into `[{Name:'db', Value:<expr>}]`,
10780
+ * fabricating drift-baseline content AWS never reported that `cdkd drift
10781
+ * --revert` then pushes to the live resource. Redaction may not buy itself a
10782
+ * fabricated baseline.
10783
+ *
10784
+ * The MIXED row is the shape this module itself calls DOMINANT for CDK — an
10785
+ * `Fn::Join` around `secret.secretValueFromJson(...)`.
10786
+ *
10787
+ * TAKE SOURCE rather than a {@link SECRET_MASK} on the rows it does close, for
10788
+ * the same reason the whole-token arm does: a mask is not a value `cdkd drift`
10789
+ * can re-resolve, so it would report a permanent phantom — and `cdkd drift
10790
+ * --revert` pushes the BASELINE to AWS, so a masked baseline would write the
10791
+ * literal `***` onto the live resource (the issue #1498 / #1501 class).
10792
+ *
10793
+ * KNOWN RESIDUAL, the last row: an observed KEY the source object does not
10794
+ * carry has no source leaf to take and no needle to match. It is NOT refused
10795
+ * the way an unpaired array ELEMENT is, and the asymmetry is deliberate rather
10796
+ * than an oversight — an extra array element is a PEER of the secret-bearing
10797
+ * ones (another `Environment` entry), so suspicion is warranted and extras are
10798
+ * rare, while an extra object KEY is a different FIELD entirely (`Runtime`,
10799
+ * `FunctionArn`, `LastModified`) and is the NORM in an AWS readback. Refusing
10800
+ * those would empty the drift baseline of every secret-bearing resource.
10801
+ * Tracked as issue [#2012](https://github.com/go-to-k/cdkd/issues/2012).
10802
+ */
10803
+ function refuseUncertifiedReadbackPositions(bag, source, secrets) {
10804
+ if (isDynamicReferenceString(source) && typeof bag === "string") {
10805
+ if (isSingleDynamicReferenceToken(source)) return source;
10806
+ if (mixedLeafMayCarryPublicReference(source, secrets)) return bag;
10807
+ return source;
10808
+ }
10809
+ if (!subtreeHasDynamicReference(source)) return bag;
10810
+ if (isPlainObject$2(bag) && isPlainObject$2(source)) {
10811
+ const out = Object.create(null);
10812
+ for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? refuseUncertifiedReadbackPositions(v, source[k], secrets) : v;
10813
+ return out;
10814
+ }
10815
+ if (Array.isArray(bag) && Array.isArray(source)) {
10816
+ const key = identityKeyFor(bag, source);
10817
+ if (key === void 0) return bag;
10818
+ const sourceByIdentity = /* @__PURE__ */ new Map();
10819
+ for (const item of source) sourceByIdentity.set(item[key], item);
10820
+ return bag.map((item) => {
10821
+ const partner = sourceByIdentity.get(item[key]);
10822
+ return partner === void 0 ? item : refuseUncertifiedReadbackPositions(item, partner, secrets);
10823
+ });
10824
+ }
10825
+ return bag;
10826
+ }
10827
+ /**
10614
10828
  * Deep-clone `bag`, replacing every occurrence of a recorded secret value with
10615
10829
  * the unresolved `{{resolve:...}}` expression it came from. A string whose WHOLE
10616
10830
  * value equals a secret is replaced by that secret's expression exactly; a
@@ -10621,7 +10835,10 @@ function redactByPath(bag, source, secrets, rules, secretExpressions) {
10621
10835
  */
10622
10836
  function redactSecretsForState(bag, secrets, source, rules = TEMPLATE_DERIVED_RULES) {
10623
10837
  if (secrets.size === 0 && source === void 0) return bag;
10624
- if (source !== void 0) return redactByPath(bag, source, secrets, rules, new Set(secrets.values()));
10838
+ if (source !== void 0) {
10839
+ const positioned = redactByPath(bag, source, secrets, rules, new Set(secrets.values()));
10840
+ return isReadbackProjectedFromState(rules) ? refuseUncertifiedReadbackPositions(positioned, source, secrets) : positioned;
10841
+ }
10625
10842
  const regex = buildNeedleRegex(secrets.keys());
10626
10843
  const wholeValueExpr = (s) => s === "" ? void 0 : secrets.get(s);
10627
10844
  const walk = (value) => {
@@ -10637,7 +10854,7 @@ function redactSecretsForState(bag, secrets, source, rules = TEMPLATE_DERIVED_RU
10637
10854
  }
10638
10855
  if (Array.isArray(value)) return value.map(walk);
10639
10856
  if (value !== null && typeof value === "object") {
10640
- const out = {};
10857
+ const out = Object.create(null);
10641
10858
  for (const [k, v] of Object.entries(value)) out[k] = walk(v);
10642
10859
  return out;
10643
10860
  }
@@ -15643,7 +15860,7 @@ var CloudControlProvider = class {
15643
15860
  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);
15644
15861
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
15645
15862
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
15646
- const { ASGProvider } = await import("./asg-provider-DOdJgPLS.js").then((n) => n.n);
15863
+ const { ASGProvider } = await import("./asg-provider-jYyUCsx_.js").then((n) => n.n);
15647
15864
  return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
15648
15865
  }
15649
15866
  const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
@@ -23221,7 +23438,7 @@ const FLUSH_INTERVAL_MS = 2e3;
23221
23438
  const FLUSH_EVENT_THRESHOLD = 50;
23222
23439
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
23223
23440
  function getCdkdVersion() {
23224
- return "0.284.7";
23441
+ return "0.284.8";
23225
23442
  }
23226
23443
  /**
23227
23444
  * Generate a time-sortable unique run id, e.g.
@@ -25690,4 +25907,4 @@ var DeployEngine = class {
25690
25907
 
25691
25908
  //#endregion
25692
25909
  export { IntrinsicFunctionResolver as $, StackHasActiveImportsError as $n, validateContainerRepoName as $t, formatResourceLine as A, AssemblyReader as An, describeTypeWithThrottleRetry as At, isExportAliasCollision as B, ConfigError as Bn, WorkGraph as Bt, refusesFinalSnapshot as C, MIGRATE_TMP_PREFIX as Cn, s3BucketDomainName as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, PARTITION_TABLE as Dn, applyRoleArnIfSet as Dt, extractDeploymentEventError as E, expectedOwnerParam as En, s3BucketWebsiteUrl as Et, red as F, getAwsClients as Fn, S3StateBackend as Ft, clearOnUpdateRemoval as G, LocalStartServiceError as Gn, escapeRegExp$1 as Gt, stateKeySecretExposure as H, DeployCancelledError as Hn, createAssetRedirectResolver as Ht, yellow as I, resetAwsClients as In, rebuildClientForBucketRegion as It, findSilentDropProperties as J, NestedStackChildDirectDestroyError as Jn, BOOTSTRAP_MARKER_PREFIX as Jt, ProviderRegistry as K, LockError as Kn, stripControlChars as Kt, collectDeclaredOutputNames as L, setAwsClients as Ln, shouldRetainResource as Lt, cyan as M, clearBucketRegionCache as Mn, DagBuilder as Mt, gray as N, resolveBucketRegion as Nn, TemplateParser as Nt, isStatefulRecreateTargetSync as O, canonicalizeRegion as On, DiffCalculator as Ot, green as P, AwsClients as Pn, LockManager as Pt, isTerminationProtectionPropagationError as Q, ResourceUpdateNotSupportedError as Qn, validateAssetBucketName as Qt, collectPublishedOutputNames as R, AssetError as Rn, AssetPublisher as Rt, isFinalSnapshotError as S, CFN_TEMPLATE_URL_LIMIT as Sn, s3BucketArn as St, makeCanonicalizePropertiesFn as T, uploadCfnTemplate as Tn, s3BucketRegionalDomainName as Tt, IAMRoleProvider as U, LocalInvokeBuildError as Un, loadPublishableAssetManifest as Ut, secretBearingStateKeyWarning as V, DependencyError as Vn, buildAssetRedirectMap as Vt, collectInlinePolicyNamesManagedBySiblings as W, LocalMigrateError as Wn, rewriteTemplateAssetReferences as Wt, slowCcOperationTimeoutMs as X, ProvisioningError as Xn, getBootstrapMarkerKey as Xt, CloudControlProvider as Y, PartialFailureError as Yn, ensureAssetStorage as Yt, disableInstanceApiTermination as Z, ResourceTimeoutError as Zn, parseBootstrapMarker as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, resolveStateBucketWithDefaultAndSource as _n, TEMPLATE_SOURCED_RULES as _t, DeploymentEventsStore as a, runDockerStreaming as an, normalizeAwsError as ar, resolveExplicitPhysicalId as at, ccRoutedFinalSnapshotError as b, warnDeprecatedNoPrefixCliFlag as bn, redactSecretsForState as bt, replayFailedOperations as c, Synthesizer as cn, isRetryableTransientError as cr, configBooleanRefusal as ct, updatePartialReason as d, getLegacyStateBucketName as dn, __exportAll as dr, replayWarn as dt, buildDenyExternalAccessPolicy as en, StackTerminationProtectionError as er, cfnRefValueFromPhysicalId as et, UNSPECIFIED_SKIP_REASON as f, resolveApp as fn, requireConfigArray as ft, computeImplicitDeleteEdges as g, resolveStateBucketWithDefault as gn, STATE_SOURCED_READBACK_RULES as gt, IMPLICIT_DELETE_DEPENDENCIES as h, resolveSkipPrefix as hn, STATE_SOURCED_CROSS_GENERATION_RULES as ht, DeploymentEventsReader as i, runDockerForeground as in, isCdkdError as ir, normalizeAwsTagsToCfn as it, bold as j, processStackMessages as jn, withRetry as jt, renderStatefulReason as k, derivePartitionAndUrlSuffix as kn, INTRINSIC_KEYS as kt, replayRollback as l, synthesisStatusMessage as ln, isThrottlingError as lr, configStringRefusal as lt, withResourceDeadline as m, resolveCaptureObservedState as mn, requireConfigString as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, formatDockerLoginError as nn, SynthesisError as nr, refStateLookupFromResource as nt, planFailedOps as o, AssetManifestLoader as on, withErrorHandling as or, assertRegionMatch as ot, deleteSkipReason as p, resolveAutoAssetStorage as pn, requireConfigObject as pt, findActionableSilentDrops as q, MissingCdkCliError as qn, AssetModeResolver as qt, DeployEngine as r, getDockerCmd as rn, formatError as rr, WAFv2WebACLProvider as rt, planRollback as s, getDockerImageBySourceHash as sn, isMarkedNonRetryable as sr, coerceCfnBoolean as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, buildDockerImage as tn, StateError as tr, getAccountInfo as tt, updatePartialMessage as u, getDefaultStateBucketName as un, markNonRetryable as ur, readConfigString as ut, PRE_DELETE_SNAPSHOT_TYPES as v, resolveUseCdkBootstrapAssets as vn, createSecretMasker as vt, unsupportedFinalSnapshotError as w, findLargeInlineResources as wn, s3BucketDualStackDomainName as wt, createPreDeleteFinalSnapshot as x, CFN_TEMPLATE_BODY_LIMIT as xn, scrubResourceRecord as xt, buildFinalSnapshotIdentifier as y, stateBucketExistenceConfirmed as yn, maskSecretsInText as yt, exportAliasCollisionScrubWarning as z, CdkdError as zn, stringifyValue as zt };
25693
- //# sourceMappingURL=deploy-engine-CJncZQEi.js.map
25910
+ //# sourceMappingURL=deploy-engine-36yWG1OT.js.map