@go-to-k/cdkd 0.284.9 → 0.284.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as IntrinsicFunctionResolver, $n as StackHasActiveImportsError, $t as validateContainerRepoName, A as formatResourceLine, An as AssemblyReader, At as describeTypeWithThrottleRetry, B as isExportAliasCollision, Bt as WorkGraph, C as refusesFinalSnapshot, Cn as MIGRATE_TMP_PREFIX, Ct as s3BucketDomainName, D as MULTI_REGION_RECREATE_BLOCKED_TYPES, Dn as PARTITION_TABLE, Dt as applyRoleArnIfSet, E as extractDeploymentEventError, En as expectedOwnerParam, Et as s3BucketWebsiteUrl, F as red, Fn as getAwsClients, Ft as S3StateBackend, G as clearOnUpdateRemoval, Gn as LocalStartServiceError, Gt as escapeRegExp, H as stateKeySecretExposure, Hn as DeployCancelledError, Ht as createAssetRedirectResolver, I as yellow, In as resetAwsClients, It as rebuildClientForBucketRegion, J as findSilentDropProperties, Jn as NestedStackChildDirectDestroyError, Jt as BOOTSTRAP_MARKER_PREFIX, K as ProviderRegistry, Kt as stripControlChars, L as collectDeclaredOutputNames, Ln as setAwsClients, Lt as shouldRetainResource, M as cyan, Mt as DagBuilder, N as gray, Nn as resolveBucketRegion, Nt as TemplateParser, O as isStatefulRecreateTargetSync, On as canonicalizeRegion, Ot as DiffCalculator, P as green, Pn as AwsClients, Pt as LockManager, Q as isTerminationProtectionPropagationError, Qn as ResourceUpdateNotSupportedError, Qt as validateAssetBucketName, R as collectPublishedOutputNames, Rt as AssetPublisher, S as isFinalSnapshotError, Sn as CFN_TEMPLATE_URL_LIMIT, St as s3BucketArn, T as makeCanonicalizePropertiesFn, Tn as uploadCfnTemplate, Tt as s3BucketRegionalDomainName, U as IAMRoleProvider, Un as LocalInvokeBuildError$1, Ut as loadPublishableAssetManifest, V as secretBearingStateKeyWarning, Vt as buildAssetRedirectMap, W as collectInlinePolicyNamesManagedBySiblings, Wn as LocalMigrateError, Wt as rewriteTemplateAssetReferences, X as slowCcOperationTimeoutMs, Xn as ProvisioningError, Xt as getBootstrapMarkerKey, Y as CloudControlProvider, Yn as PartialFailureError, Yt as ensureAssetStorage, Z as disableInstanceApiTermination, Zn as ResourceTimeoutError, Zt as parseBootstrapMarker, _ as ATOMIC_FINAL_SNAPSHOT_TYPES, _n as resolveStateBucketWithDefaultAndSource, _t as TEMPLATE_SOURCED_RULES, a as DeploymentEventsStore, an as runDockerStreaming, ar as normalizeAwsError, at as resolveExplicitPhysicalId, b as ccRoutedFinalSnapshotError, bn as warnDeprecatedNoPrefixCliFlag, bt as redactSecretsForState, c as replayFailedOperations, cn as Synthesizer, cr as isRetryableTransientError, ct as configBooleanRefusal, d as updatePartialReason, dn as getLegacyStateBucketName, dt as replayWarn, en as buildDenyExternalAccessPolicy, er as StackTerminationProtectionError, et as cfnRefValueFromPhysicalId, f as UNSPECIFIED_SKIP_REASON, fn as resolveApp, ft as requireConfigArray, g as computeImplicitDeleteEdges, gn as resolveStateBucketWithDefault, gt as STATE_SOURCED_READBACK_RULES, h as IMPLICIT_DELETE_DEPENDENCIES, hn as resolveSkipPrefix, ht as STATE_SOURCED_CROSS_GENERATION_RULES, i as DeploymentEventsReader, in as runDockerForeground, it as normalizeAwsTagsToCfn, j as bold, jn as processStackMessages, jt as withRetry, k as renderStatefulReason, kn as derivePartitionAndUrlSuffix, kt as INTRINSIC_KEYS, l as replayRollback, ln as synthesisStatusMessage, lr as isThrottlingError, lt as configStringRefusal, m as withResourceDeadline, mn as resolveCaptureObservedState, mt as requireConfigString, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as formatDockerLoginError, nt as refStateLookupFromResource, o as planFailedOps, on as AssetManifestLoader, or as withErrorHandling, ot as assertRegionMatch, p as deleteSkipReason, pn as resolveAutoAssetStorage, pt as requireConfigObject, q as findActionableSilentDrops, qn as MissingCdkCliError, qt as AssetModeResolver, r as DeployEngine, rn as getDockerCmd, rt as WAFv2WebACLProvider, s as planRollback, sn as getDockerImageBySourceHash, sr as isMarkedNonRetryable, st as coerceCfnBoolean, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as buildDockerImage, tt as getAccountInfo, u as updatePartialMessage, un as getDefaultStateBucketName, ur as markNonRetryable, ut as readConfigString, v as PRE_DELETE_SNAPSHOT_TYPES, vn as resolveUseCdkBootstrapAssets, vt as createSecretMasker, w as unsupportedFinalSnapshotError, wn as findLargeInlineResources, wt as s3BucketDualStackDomainName, x as createPreDeleteFinalSnapshot, xn as CFN_TEMPLATE_BODY_LIMIT, xt as scrubResourceRecord, y as buildFinalSnapshotIdentifier, yn as stateBucketExistenceConfirmed, yt as maskSecretsInText, z as exportAliasCollisionScrubWarning, zn as CdkdError, zt as stringifyValue } from "./deploy-engine-CY2fx4K1.js";
2
+ import { $ as IntrinsicFunctionResolver, $n as StackHasActiveImportsError, $t as validateContainerRepoName, A as formatResourceLine, An as AssemblyReader, At as describeTypeWithThrottleRetry, B as isExportAliasCollision, Bt as WorkGraph, C as refusesFinalSnapshot, Cn as MIGRATE_TMP_PREFIX, Ct as s3BucketDomainName, D as MULTI_REGION_RECREATE_BLOCKED_TYPES, Dn as PARTITION_TABLE, Dt as applyRoleArnIfSet, E as extractDeploymentEventError, En as expectedOwnerParam, Et as s3BucketWebsiteUrl, F as red, Fn as getAwsClients, Ft as S3StateBackend, G as clearOnUpdateRemoval, Gn as LocalStartServiceError, Gt as escapeRegExp, H as stateKeySecretExposure, Hn as DeployCancelledError, Ht as createAssetRedirectResolver, I as yellow, In as resetAwsClients, It as rebuildClientForBucketRegion, J as findSilentDropProperties, Jn as NestedStackChildDirectDestroyError, Jt as BOOTSTRAP_MARKER_PREFIX, K as ProviderRegistry, Kt as stripControlChars, L as collectDeclaredOutputNames, Ln as setAwsClients, Lt as shouldRetainResource, M as cyan, Mt as DagBuilder, N as gray, Nn as resolveBucketRegion, Nt as TemplateParser, O as isStatefulRecreateTargetSync, On as canonicalizeRegion, Ot as DiffCalculator, P as green, Pn as AwsClients, Pt as LockManager, Q as isTerminationProtectionPropagationError, Qn as ResourceUpdateNotSupportedError, Qt as validateAssetBucketName, R as collectPublishedOutputNames, Rt as AssetPublisher, S as isFinalSnapshotError, Sn as CFN_TEMPLATE_URL_LIMIT, St as s3BucketArn, T as makeCanonicalizePropertiesFn, Tn as uploadCfnTemplate, Tt as s3BucketRegionalDomainName, U as IAMRoleProvider, Un as LocalInvokeBuildError$1, Ut as loadPublishableAssetManifest, V as secretBearingStateKeyWarning, Vt as buildAssetRedirectMap, W as collectInlinePolicyNamesManagedBySiblings, Wn as LocalMigrateError, Wt as rewriteTemplateAssetReferences, X as slowCcOperationTimeoutMs, Xn as ProvisioningError, Xt as getBootstrapMarkerKey, Y as CloudControlProvider, Yn as PartialFailureError, Yt as ensureAssetStorage, Z as disableInstanceApiTermination, Zn as ResourceTimeoutError, Zt as parseBootstrapMarker, _ as ATOMIC_FINAL_SNAPSHOT_TYPES, _n as resolveStateBucketWithDefaultAndSource, _t as TEMPLATE_SOURCED_RULES, a as DeploymentEventsStore, an as runDockerStreaming, ar as normalizeAwsError, at as resolveExplicitPhysicalId, b as ccRoutedFinalSnapshotError, bn as warnDeprecatedNoPrefixCliFlag, bt as redactSecretsForState, c as replayFailedOperations, cn as Synthesizer, cr as isRetryableTransientError, ct as configBooleanRefusal, d as updatePartialReason, dn as getLegacyStateBucketName, dt as replayWarn, en as buildDenyExternalAccessPolicy, er as StackTerminationProtectionError, et as cfnRefValueFromPhysicalId, f as UNSPECIFIED_SKIP_REASON, fn as resolveApp, ft as requireConfigArray, g as computeImplicitDeleteEdges, gn as resolveStateBucketWithDefault, gt as STATE_SOURCED_READBACK_RULES, h as IMPLICIT_DELETE_DEPENDENCIES, hn as resolveSkipPrefix, ht as STATE_SOURCED_CROSS_GENERATION_RULES, i as DeploymentEventsReader, in as runDockerForeground, it as normalizeAwsTagsToCfn, j as bold, jn as processStackMessages, jt as withRetry, k as renderStatefulReason, kn as derivePartitionAndUrlSuffix, kt as INTRINSIC_KEYS, l as replayRollback, ln as synthesisStatusMessage, lr as isThrottlingError, lt as configStringRefusal, m as withResourceDeadline, mn as resolveCaptureObservedState, mt as requireConfigString, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as formatDockerLoginError, nt as refStateLookupFromResource, o as planFailedOps, on as AssetManifestLoader, or as withErrorHandling, ot as assertRegionMatch, p as deleteSkipReason, pn as resolveAutoAssetStorage, pt as requireConfigObject, q as findActionableSilentDrops, qn as MissingCdkCliError, qt as AssetModeResolver, r as DeployEngine, rn as getDockerCmd, rt as WAFv2WebACLProvider, s as planRollback, sn as getDockerImageBySourceHash, sr as isMarkedNonRetryable, st as coerceCfnBoolean, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as buildDockerImage, tt as getAccountInfo, u as updatePartialMessage, un as getDefaultStateBucketName, ur as markNonRetryable, ut as readConfigString, v as PRE_DELETE_SNAPSHOT_TYPES, vn as resolveUseCdkBootstrapAssets, vt as createSecretMasker, w as unsupportedFinalSnapshotError, wn as findLargeInlineResources, wt as s3BucketDualStackDomainName, x as createPreDeleteFinalSnapshot, xn as CFN_TEMPLATE_BODY_LIMIT, xt as scrubResourceRecord, y as buildFinalSnapshotIdentifier, yn as stateBucketExistenceConfirmed, yt as maskSecretsInText, z as exportAliasCollisionScrubWarning, zn as CdkdError, zt as stringifyValue } from "./deploy-engine-DGuaeWaV.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
4
- import { t as ASGProvider } from "./asg-provider-B09ceB1r.js";
4
+ import { t as ASGProvider } from "./asg-provider-ChVCjyK2.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
7
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -60686,6 +60686,36 @@ var ScrubNeededError = class ScrubNeededError extends CdkdError {
60686
60686
  * parameter; scrub only stops it from being re-read out of state going
60687
60687
  * forward.
60688
60688
  *
60689
+ * OUTPUTS: `state.outputs` is scrubbed alongside the resource records, and its
60690
+ * repair scope is WIDER than today's declared outputs (issue #2005). A stored
60691
+ * output key the template can still name is redacted BY POSITION against that
60692
+ * template; a key it cannot name is repaired when its stored value MATCHES a
60693
+ * secret plaintext recorded anywhere this run, including one only a RESOURCE
60694
+ * still references. The motivating member of that population is an output
60695
+ * DELETED in an ordinary refactor, but it is not the only one — a key this run
60696
+ * cannot COMPUTE is in it too, and a parameterized `Export.Name` (scrub has
60697
+ * only template defaults) leaves the deploy's real alias key unaccounted on
60698
+ * EVERY run. When nothing recorded the plaintext the value is left exactly as
60699
+ * it is: a scrub that cannot identify the needle must not guess, because
60700
+ * `state.outputs` is re-applied VERBATIM to consumer stacks — by the exports
60701
+ * index (`src/state/export-index-store.ts`) and by `Fn::ImportValue` /
60702
+ * `Fn::GetStackOutput` (`src/deployment/intrinsic-function-resolver.ts`) — so a
60703
+ * fabricated redaction ships a literal `{{resolve:...}}` token into a
60704
+ * consumer's own AWS call. (`cdkd drift` is NOT one of those readers: it reads
60705
+ * `state.resources`, never `state.outputs`.) See `redactUnaccountedOutputs`.
60706
+ *
60707
+ * A FOURTH re-apply reader exists and is recorded here so the next audit does
60708
+ * not have to re-derive it: `NestedStackProvider.buildOutputsAttributes`
60709
+ * projects a CHILD record's `state.outputs` into the PARENT's
60710
+ * `Fn::GetAtt Outputs.X` attributes, i.e. into a parent resource property. It
60711
+ * is OUT of this command's reach, not exempt from the hazard — `scrubCommand`
60712
+ * targets synth STACK ARTIFACTS, and a nested child is a `nestedTemplates`
60713
+ * entry on its parent's `StackInfo` rather than an `aws:cloudformation:stack`
60714
+ * artifact of its own (`src/synthesis/assembly-reader.ts`), so the
60715
+ * `{parent}~{Child}` record this walk reads is never a stack scrub writes. If
60716
+ * scrub ever gains nested-child targets, this reader joins the list above and
60717
+ * the fabrication bound has to be re-argued for it.
60718
+ *
60689
60719
  * ORDERING: scrub matches the CURRENT resolved secret value against what state
60690
60720
  * holds, so run it BEFORE rotating. Once the secret is rotated, the value in
60691
60721
  * state no longer matches the current one and scrub cannot find it (it reports
@@ -60776,11 +60806,208 @@ async function scrubCommand(stacks, options) {
60776
60806
  if (options.fail && totalStacksWithUnscrubbableKeys > 0) throw new ScrubNeededError();
60777
60807
  }
60778
60808
  /**
60809
+ * Every secret plaintext this run recorded, from ANY position in the template
60810
+ * (issue #2005) — the outputs' own map plus every resource's, filtered to the
60811
+ * values long enough to be a safe needle.
60812
+ *
60813
+ * Used ONLY by {@link redactUnaccountedOutputs}, which is why the union is built
60814
+ * here rather than kept as the pass's input everywhere: `outputSecrets` and
60815
+ * `perResourceSecrets` are deliberately SEPARATE bags so one resource's secret
60816
+ * value cannot rewrite another resource's coinciding literal (the collision the
60817
+ * deploy engine's `perResourceSecrets` doc describes), and widening the bag the
60818
+ * RESOURCE walk uses would re-open exactly that.
60819
+ *
60820
+ * On a value collision the outputs' expression wins: it is written last, and an
60821
+ * output KEY is the closest kin of the bag this map is scanned against. Both
60822
+ * expressions resolve to the same plaintext by construction (that is what makes
60823
+ * them collide), so the choice costs precision, not correctness — the residual
60824
+ * documented for the ambiguous-key fallback one screen up. A resource-vs-
60825
+ * resource collision is NOT ordered by anything: `perResourceSecrets` is
60826
+ * iterated in map order and the LAST logical id wins, so between two resources
60827
+ * resolving one plaintext the surviving expression is whichever the walk
60828
+ * reached last. Same trade as above (both resolve to the same value), stated
60829
+ * because only the outputs-vs-resource half used to be.
60830
+ *
60831
+ * **The needle floor is the security half, and it looks like it contradicts
60832
+ * repo policy without doing so.** `redactSecretsForState`'s no-source arm
60833
+ * matches a WHOLE VALUE at ANY length; only its substring arm honors
60834
+ * {@link MIN_NEEDLE_LENGTH}. That is sound for a POSITION-SCOPED bag, where the
60835
+ * candidate leaf is already known to belong to the reference. This union has no
60836
+ * position source at all, so an unfiltered 1-3 character plaintext recorded by
60837
+ * ANY resource would whole-value-match an unrelated stored output and rewrite
60838
+ * it onto that resource's expression. Issues #2012 / #2036 do say that with no
60839
+ * evidence OVER-redaction is the right failure, because it is "visible,
60840
+ * recoverable, and not a disclosure" — but that asymmetry is about a DRIFT
60841
+ * BASELINE, and it does not transfer here: `state.outputs` is re-applied
60842
+ * VERBATIM to consumer stacks by the exports index
60843
+ * (`src/state/export-index-store.ts`) and by `Fn::ImportValue` /
60844
+ * `Fn::GetStackOutput` (`src/deployment/intrinsic-function-resolver.ts`), so a
60845
+ * false redaction ships a literal `{{resolve:...}}` token into a consumer's own
60846
+ * AWS call — the #1934 class, a BREAK rather than a recoverable mismatch. (Two
60847
+ * readers, not the whole list: the module doc records a third that is out of
60848
+ * this command's reach, with the derivation.)
60849
+ *
60850
+ * The floor is applied to the WHOLE union, `outputSecrets` included, and that
60851
+ * narrows NOTHING that worked before. The positioning pass still runs FIRST
60852
+ * over the same bag with the UNFILTERED `outputSecrets`, whole-value-matching
60853
+ * at any length, and {@link redactUnaccountedOutputs} scans the STORED value —
60854
+ * so a sub-floor output secret it cannot see compares equal, falls through, and
60855
+ * the positioned leaf survives into `repaired ?? positioned`. Pinned by the
60856
+ * suite's "still repairs a SCALAR unaccounted key holding a sub-floor OUTPUT
60857
+ * secret" case (`ab1` -> its expression — the exact shape the false note
60858
+ * described) and by its non-scalar sibling "keeps the POSITIONED value". The
60859
+ * floor's effect is therefore confined to CROSS-RESOURCE needles: a sub-floor
60860
+ * plaintext recorded only by a RESOURCE never becomes a needle for the outputs
60861
+ * bag. Stated because an earlier revision claimed a narrowing here, and a false
60862
+ * "we gave this up" note is how a capability gets removed for real later.
60863
+ *
60864
+ * **What the floor does NOT bound, stated plainly because it is the residual a
60865
+ * reader will otherwise assume away.** `redactSecretsForState`'s SUBSTRING arm
60866
+ * runs over this union too, so a recorded plaintext of {@link
60867
+ * MIN_NEEDLE_LENGTH} characters or more occurring ANYWHERE inside an unrelated
60868
+ * unaccounted output WILL be rewritten. `secretValueFromJson('username')`
60869
+ * resolving `admin` turns a stored `https://admin.example.com` into
60870
+ * `https://{{resolve:secretsmanager:...}}.example.com`, which the exports index
60871
+ * republishes and a consumer's `Fn::ImportValue` then ships to AWS as a literal
60872
+ * token — the #1934 BREAK class, from a value that was never a secret.
60873
+ *
60874
+ * That arm is kept, and raising the floor for this union alone was considered
60875
+ * and REJECTED, on three grounds:
60876
+ *
60877
+ * - **Substring matching is what makes the repair work at all.** A connection
60878
+ * string embedding a password is #2005's own target shape and this suite's
60879
+ * own `DbUrl` case; gutting the arm would leave the motivating population
60880
+ * unrepairable, which is the disclosure the command exists to remove.
60881
+ * - **Length is a weak proxy for the hazard, in both directions.** The values
60882
+ * that collide with unrelated text are common tokens — `admin`, `prod`,
60883
+ * `root` — and raising the bar to 8 admits `password`, `postgres`,
60884
+ * `localhost`, which are longer AND likelier to occur inside a stored URL. A
60885
+ * higher floor would refuse genuine short secrets while still admitting the
60886
+ * colliding ones.
60887
+ * - **4 is the repo's ONE answer, and a second number would have no
60888
+ * derivation.** `secret-redaction.ts`'s own needles, `stateKeySecretExposure`
60889
+ * (#1919) and `drift.ts`'s `carriesRecordedSecret` all use it. A union-only 8
60890
+ * would be the only such constant in the tree justified by taste, and a later
60891
+ * reader could not tell which is authoritative.
60892
+ *
60893
+ * The trade is right for a GHSA repair command because the two failure
60894
+ * directions differ on REVERSIBILITY, not on likelihood. A fabricated rewrite
60895
+ * is loud and remediable: the consumer's next deploy fails on an unresolvable
60896
+ * token, and the operator fixes the template or edits the key out of state. An
60897
+ * unrepaired plaintext is silent and permanent — the key is one today's
60898
+ * template does not declare, so no redeploy ever rewrites it, and no other
60899
+ * command can. Refusing the substring arm here would trade the irreversible
60900
+ * failure for the reversible one in the wrong direction.
60901
+ */
60902
+ function allRecordedSecrets(outputSecrets, perResourceSecrets) {
60903
+ const union = /* @__PURE__ */ new Map();
60904
+ for (const recorded of perResourceSecrets.values()) for (const [value, expression] of recorded) union.set(value, expression);
60905
+ for (const [value, expression] of outputSecrets) union.set(value, expression);
60906
+ for (const value of union.keys()) if (value.length < 4) union.delete(value);
60907
+ return union;
60908
+ }
60909
+ /**
60910
+ * Repair a stored output key today's template cannot ACCOUNT for (issue
60911
+ * [#2005](https://github.com/go-to-k/cdkd/issues/2005)) — the population `cdkd
60912
+ * scrub` is documented as the remedy for and could not actually remedy.
60913
+ *
60914
+ * `outputSecrets` is built from today's DECLARED outputs, and the outputs
60915
+ * redaction above is gated on it being non-empty. An output DELETED from the
60916
+ * template contributes nothing to that set, so a record whose only
60917
+ * secret-bearing output has since been removed was reported CLEAN by the
60918
+ * command whose job is to clean it, while the plaintext stayed in
60919
+ * `state.outputs` until an unrelated deploy happened to rewrite the bag. That
60920
+ * is precisely the record `cdkd diff` withholds from display
60921
+ * ([#1948](https://github.com/go-to-k/cdkd/issues/1948)): correctly hidden, and
60922
+ * until now not repairable.
60923
+ *
60924
+ * TWO scope decisions, and both are load-bearing.
60925
+ *
60926
+ * 1. **WHICH keys.** Only keys today's template cannot name — not in
60927
+ * `accountedKeys` (every declared output name plus every `Export.Name` this
60928
+ * run could compute). A key the template DOES name is left exactly as the
60929
+ * pass above leaves it, deliberately, because that pass POSITIONS it against
60930
+ * the template and this one cannot: scanning an accounted key against the
60931
+ * union bag would let one resource's secret plaintext rewrite a declared
60932
+ * output's coinciding literal onto that resource's expression — a value
60933
+ * state was never meant to hold, in the command that exists to make state
60934
+ * trustworthy. The accepted residual, stated because narrowing is what buys
60935
+ * the safety: a DECLARED output whose template value no longer resolves a
60936
+ * secret, but whose STORED value is still the stale plaintext of one a
60937
+ * resource carries, is not repaired here either. A redeploy rewrites it.
60938
+ *
60939
+ * 2. **WHAT may be rewritten.** Only a value that genuinely MATCHES a recorded
60940
+ * plaintext — `redactSecretsForState`'s value scan with no source, i.e. a
60941
+ * whole-value match and an embedded one, both bounded at
60942
+ * {@link MIN_NEEDLE_LENGTH} by {@link allRecordedSecrets}. A scrub that
60943
+ * cannot identify the needle must not guess: `state.outputs` is re-applied
60944
+ * VERBATIM to consumer stacks by the exports index and by `Fn::ImportValue`
60945
+ * / `Fn::GetStackOutput` (and by one more reader out of this command's
60946
+ * reach — see the module doc), so a fabricated redaction ships a literal
60947
+ * `{{resolve:...}}` token into a consumer's AWS call. No key is invented and
60948
+ * no key is removed for the same reason. When nothing this run recorded the
60949
+ * plaintext (the secret was deleted, rotated away, or the reference is gone
60950
+ * from the template too) the value is LEFT ALONE — but note that behavior
60951
+ * does NOT live in this function's `secrets.size === 0` guard, which is one
60952
+ * of three redundant reasons and fences none of them; see the guard's own
60953
+ * comment for what actually reaches it.
60954
+ *
60955
+ * **The scanned VALUE is the STORED one, never the positioned pass's output**
60956
+ * (blocker found in review). Scanning an already-positioned leaf means scanning
60957
+ * an expression the first pass just INSERTED, and `redactSecretsForState`'s
60958
+ * token guard protects only a WHOLE-VALUE token — so a MIXED leaf
60959
+ * (`postgres://admin:{{resolve:secretsmanager:prod/db:...}}@app-db`, the
60960
+ * `Fn::Join` shape CDK emits) gets re-scanned and any union needle occurring
60961
+ * INSIDE the inserted reference is spliced into it, corrupting the expression
60962
+ * into one no service can resolve (the corruption `secret-redaction.ts`'s own
60963
+ * token guard documents). A single union scan of the STORED value subsumes the
60964
+ * first pass for these keys rather than composing with it: the union is a
60965
+ * superset of `outputSecrets` with the outputs' expression winning a collision,
60966
+ * and an unaccounted key by construction has no position source in
60967
+ * `outputsTemplateSource` (every key that bag carries — a declared output name
60968
+ * or a literal `Export.Name` — is in `accountedKeys`), so the first pass could
60969
+ * only ever have value-scanned it too.
60970
+ *
60971
+ * **The repaired population is WIDER than "an output you deleted."** Any stored
60972
+ * key today's template cannot COMPUTE is in it, and a parameterized
60973
+ * `Export.Name` is the standing example: `scrub` has only template defaults, so
60974
+ * a name resolving to the literal `prefix-${Foo}` leaves the REAL alias key the
60975
+ * deploy wrote unaccounted on EVERY run, and that key gets cross-resource value
60976
+ * matching for as long as the parameter stays unresolvable here. That is the
60977
+ * accepted cost of not being able to reproduce the key — the alternative is
60978
+ * leaving a permanently unrepairable key — but it is a standing exposure to the
60979
+ * fabrication risk above rather than a one-off after a refactor.
60980
+ *
60981
+ * Returns the input by IDENTITY when nothing changed. That is worth doing on
60982
+ * its own terms (no churn, no needless clone), but it is NOT what keeps the
60983
+ * caller honest: the caller compares with `JSON.stringify` unconditionally, so
60984
+ * an identity return cannot make it cheaper and a fresh-but-equal object could
60985
+ * not make it report a repair it did not perform.
60986
+ */
60987
+ function redactUnaccountedOutputs(positioned, stored, accountedKeys, secrets) {
60988
+ if (secrets.size === 0 || !positioned || !stored) return positioned;
60989
+ let repaired;
60990
+ for (const [key, value] of Object.entries(stored)) {
60991
+ if (accountedKeys.has(key)) continue;
60992
+ const next = redactSecretsForState(value, secrets);
60993
+ if (JSON.stringify(next) === JSON.stringify(value)) continue;
60994
+ repaired ??= { ...positioned };
60995
+ repaired[key] = next;
60996
+ }
60997
+ return repaired ?? positioned;
60998
+ }
60999
+ /**
60779
61000
  * Scrub one stack's state. Re-resolves the template's per-resource properties to
60780
61001
  * learn the resolved secret VALUES, then replaces those values in the state
60781
61002
  * record with their `{{resolve:...}}` expressions. Returns counts; performs no
60782
61003
  * AWS mutation. Acquires the stack lock for the read-modify-write unless
60783
61004
  * `dryRun`.
61005
+ *
61006
+ * The stack OUTPUTS bag is scrubbed too, in two passes: today's declared
61007
+ * outputs are redacted BY POSITION against the template, and a stored key
61008
+ * today's template cannot account for is repaired by VALUE MATCH alone — see
61009
+ * {@link redactUnaccountedOutputs} for why the two are separate and what each
61010
+ * deliberately declines to do.
60784
61011
  */
60785
61012
  async function scrubStack(stack, region, stateBackend, lockManager, opts) {
60786
61013
  const { logger } = opts;
@@ -60842,6 +61069,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
60842
61069
  }
60843
61070
  const templateOutputs = stack.template.Outputs ?? {};
60844
61071
  const declaredOutputNames = collectDeclaredOutputNames(templateOutputs);
61072
+ const accountedOutputKeys = new Set(declaredOutputNames);
60845
61073
  const ambiguousKeys = /* @__PURE__ */ new Set();
60846
61074
  const collisions = [];
60847
61075
  let outputsSourceUntrusted = false;
@@ -60863,7 +61091,9 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
60863
61091
  logger.warn(`Export.Name of output ${name} could not be resolved during scrub (${err instanceof Error ? err.message : String(err)}) — redacting this stack's outputs by value match instead of by template position, since state may be keyed under a name this run cannot reproduce.`);
60864
61092
  }
60865
61093
  if (declaredExportName !== void 0 && typeof exportName !== "string") outputsSourceUntrusted = true;
60866
- if (declaredExportName !== void 0 && typeof exportName === "string" && isUnresolvedValue(exportName, templateUsesSub(declaredExportName))) {
61094
+ const exportNameUnresolved = declaredExportName !== void 0 && typeof exportName === "string" && isUnresolvedValue(exportName, templateUsesSub(declaredExportName));
61095
+ if (typeof exportName === "string" && !exportNameUnresolved) accountedOutputKeys.add(exportName);
61096
+ if (exportNameUnresolved) {
60867
61097
  outputsSourceUntrusted = true;
60868
61098
  logger.warn(`Export.Name of output ${name} did not fully resolve during scrub — redacting this stack's outputs by value match instead of by template position, since state may be keyed under a name this run cannot reproduce.`);
60869
61099
  } else if (typeof exportName === "string" && isExportAliasCollision(exportName, name, declaredOutputNames)) {
@@ -60919,7 +61149,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
60919
61149
  if (JSON.stringify(scrubbed) !== JSON.stringify(record)) recordsChanged++;
60920
61150
  newResources[logicalId] = scrubbed;
60921
61151
  }
60922
- const newOutputs = outputSecrets.size > 0 ? redactSecretsForState(state.outputs, outputSecrets, outputsSourceUntrusted ? void 0 : outputsTemplateSource) : state.outputs;
61152
+ const newOutputs = redactUnaccountedOutputs(outputSecrets.size > 0 ? redactSecretsForState(state.outputs, outputSecrets, outputsSourceUntrusted ? void 0 : outputsTemplateSource) : state.outputs, state.outputs, accountedOutputKeys, allRecordedSecrets(outputSecrets, perResourceSecrets));
60923
61153
  if (JSON.stringify(newOutputs) !== JSON.stringify(state.outputs)) recordsChanged++;
60924
61154
  if (recordsChanged > 0 && !opts.dryRun) {
60925
61155
  const nextState = {
@@ -62060,11 +62290,12 @@ function containsDynamicReference(value) {
62060
62290
  return false;
62061
62291
  }
62062
62292
  /**
62063
- * Byte-for-byte the threshold `secret-redaction.ts` applies when it builds its
62064
- * substring needles (`MIN_NEEDLE_LENGTH`). Copied rather than imported because
62065
- * that constant is module-private there and this file may not widen its
62066
- * exports; the two must agree or this function would call a leaf secret-bearing
62067
- * that `redactSecretsForState` will not redact.
62293
+ * The threshold `secret-redaction.ts` applies when it builds its substring
62294
+ * needles. IMPORTED rather than copied since issue #2005 exported it (`cdkd
62295
+ * scrub` needed the same constant to bound its cross-resource union), so the
62296
+ * two can no longer disagree — and they must not, or this function would call a
62297
+ * leaf secret-bearing that `redactSecretsForState` will not redact. The local
62298
+ * alias is kept so the call sites below read unchanged.
62068
62299
  */
62069
62300
  const MIN_SECRET_NEEDLE_LENGTH = 4;
62070
62301
  /**
@@ -82481,7 +82712,7 @@ function createMigrateCommand() {
82481
82712
  */
82482
82713
  function buildProgram() {
82483
82714
  const program = new Command();
82484
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.9");
82715
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.10");
82485
82716
  program.hook("preAction", (_thisCommand, actionCommand) => {
82486
82717
  const { profile } = actionCommand.optsWithGlobals();
82487
82718
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;