@go-to-k/cdkd 0.284.35 → 0.284.37

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 endCommandInterruptScope, $t as buildAssetRedirectMap, A as MULTI_REGION_RECREATE_BLOCKED_TYPES, An as resolveStateBucketWithDefaultAndSource, At as maskSecretsInError, B as collectDeclaredOutputNames, Bn as PARTITION_TABLE, Bt as DiffCalculator, C as ccRoutedFinalSnapshotError, Cn as getDefaultStateBucketName, Cr as isThrottlingError, Ct as STATE_SOURCED_CROSS_GENERATION_RULES, D as unsupportedFinalSnapshotError, Dn as resolveCaptureObservedState, Dt as dynamicReferenceTokens, E as refusesFinalSnapshot, En as resolveAutoAssetStorage, Et as createSecretMasker, F as cyan, Fn as CFN_TEMPLATE_URL_LIMIT, Ft as s3BucketDomainName, G as stateKeySecretExposure, Gt as TemplateParser, H as exportAliasCollisionScrubWarning, Hn as derivePartitionAndUrlSuffix, Ht as describeTypeWithThrottleRetry, I as gray, In as MIGRATE_TMP_PREFIX, It as s3BucketDualStackDomainName, J as clearOnUpdateRemoval, Jn as getAwsClients, Jt as rebuildClientForBucketRegion, K as IAMRoleProvider, Kn as resolveBucketRegion, Kt as LockManager, L as green, Ln as findLargeInlineResources, Lt as s3BucketRegionalDomainName, M as renderStatefulReason, Mn as stateBucketExistenceConfirmed, Mt as redactSecretsForState, N as formatResourceLine, Nn as warnDeprecatedNoPrefixCliFlag, Nt as scrubResourceRecord, O as makeCanonicalizePropertiesFn, On as resolveSkipPrefix, Ot as errorCauseChain, P as bold, Pn as CFN_TEMPLATE_BODY_LIMIT, Pt as s3BucketArn, Q as beginCommandInterruptScope, Qn as CdkdError, Qt as WorkGraph, R as red, Rn as uploadCfnTemplate, Rt as s3BucketWebsiteUrl, S as buildFinalSnapshotIdentifier, Sn as synthesisStatusMessage, Sr as isRetryableTransientError, St as requireConfigString, T as isFinalSnapshotError, Tn as resolveApp, Tt as TEMPLATE_SOURCED_RULES, U as isExportAliasCollision, Un as AssemblyReader, Ut as withRetry, V as collectPublishedOutputNames, Vn as canonicalizeRegion, Vt as INTRINSIC_KEYS, W as secretBearingStateKeyWarning, Wn as processStackMessages, Wt as DagBuilder, X as findActionableSilentDrops, Xn as setAwsClients, Xt as AssetPublisher, Y as ProviderRegistry, Yn as resetAwsClients, Yt as shouldRetainResource, Z as findSilentDropProperties, Zt as stringifyValue, _ as maskingRetryLogger, _n as runDockerForeground, _t as configStringRefusal, a as DeploymentEventsStore, an as AssetModeResolver, ar as LocalStartServiceError, at as isTerminationProtectionPropagationError, b as ATOMIC_FINAL_SNAPSHOT_TYPES, bn as getDockerImageBySourceHash, br as withErrorHandling, bt as requireConfigArray, c as planRollback, cn as getBootstrapMarkerKey, cr as NestedStackChildDirectDestroyError, ct as cfnRefValueFromPhysicalId, d as replayRollback, dn as validateAssetBucketName, dr as ResourceTimeoutError, dt as WAFv2WebACLProvider, en as createAssetRedirectResolver, er as CrossAccountSecretRefusalError, et as isInterruptedWaitError, f as updatePartialMessage, fn as validateContainerRepoName, fr as ResourceUpdateNotSupportedError, ft as normalizeAwsTagsToCfn, g as withResourceDeadline, gn as getDockerCmd, gt as configBooleanRefusal, h as deleteSkipReason, hn as formatDockerLoginError, ht as coerceCfnBoolean, i as DeploymentEventsReader, in as stripControlChars, ir as LocalMigrateError, it as disableInstanceApiTermination, j as isStatefulRecreateTargetSync, jn as resolveUseCdkBootstrapAssets, jt as maskSecretsInText, k as extractDeploymentEventError, kn as resolveStateBucketWithDefault, kt as isSingleDynamicReferenceToken, l as producerRegionsFromState, ln as parseBootstrapMarker, lr as PartialFailureError, lt as getAccountInfo, m as UNSPECIFIED_SKIP_REASON, mn as buildDockerImage, mr as StackTerminationProtectionError, mt as assertRegionMatch, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as rewriteTemplateAssetReferences, nr as DeployCancelledError, nt as CloudControlProvider, o as classifyReplaySecretRegion, on as BOOTSTRAP_MARKER_PREFIX, ot as IntrinsicFunctionResolver, p as updatePartialReason, pn as buildDenyExternalAccessPolicy, pr as StackHasActiveImportsError, pt as resolveExplicitPhysicalId, q as collectInlinePolicyNamesManagedBySiblings, qn as AwsClients, qt as S3StateBackend, r as DeployEngine, rn as escapeRegExp, rr as LocalInvokeBuildError$1, rt as slowCcOperationTimeoutMs, s as planFailedOps, sn as ensureAssetStorage, sr as MissingCdkCliError, st as carriesDynamicReference, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as loadPublishableAssetManifest, tt as startInterruptWatch, u as replayFailedOperations, un as readBootstrapMarkerBody, ur as ProvisioningError, ut as refStateLookupFromResource, v as IMPLICIT_DELETE_DEPENDENCIES, vn as runDockerStreaming, vt as readConfigString, w as createPreDeleteFinalSnapshot, wn as getLegacyStateBucketName, wr as markNonRetryable, wt as STATE_SOURCED_READBACK_RULES, x as PRE_DELETE_SNAPSHOT_TYPES, xn as Synthesizer, xr as isMarkedNonRetryable, xt as requireConfigObject, y as computeImplicitDeleteEdges, yn as AssetManifestLoader, yr as normalizeAwsError, yt as replayWarn, z as yellow, zn as expectedOwnerParam, zt as applyRoleArnIfSet } from "./deploy-engine-Wei5Yw-_.js";
2
+ import { $ as startInterruptWatch, $t as buildAssetRedirectMap, A as renderStatefulReason, An as resolveStateBucketWithDefaultAndSource, At as redactSecretsForState, B as exportAliasCollisionScrubWarning, Bn as PARTITION_TABLE, Bt as DiffCalculator, C as isFinalSnapshotError, Cn as getDefaultStateBucketName, Cr as isRetryableTransientError, Ct as TEMPLATE_SOURCED_RULES, D as extractDeploymentEventError, Dn as resolveCaptureObservedState, Dt as isSingleDynamicReferenceToken, E as makeCanonicalizePropertiesFn, En as resolveAutoAssetStorage, Et as errorCauseChain, F as green, Fn as CFN_TEMPLATE_URL_LIMIT, Ft as s3BucketDomainName, G as collectInlinePolicyNamesManagedBySiblings, Gt as TemplateParser, H as secretBearingStateKeyWarning, Hn as derivePartitionAndUrlSuffix, Ht as describeTypeWithThrottleRetry, I as red, In as MIGRATE_TMP_PREFIX, It as s3BucketDualStackDomainName, J as findActionableSilentDrops, Jn as getAwsClients, Jt as rebuildClientForBucketRegion, K as clearOnUpdateRemoval, Kn as resolveBucketRegion, Kt as LockManager, L as yellow, Ln as findLargeInlineResources, Lt as s3BucketRegionalDomainName, M as bold, Mn as stateBucketExistenceConfirmed, Mt as classifyReplaySecretRegion, N as cyan, Nn as warnDeprecatedNoPrefixCliFlag, Nt as producerRegionsFromState, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as resolveSkipPrefix, Ot as maskSecretsInError, P as gray, Pn as CFN_TEMPLATE_BODY_LIMIT, Pt as s3BucketArn, Q as isInterruptedWaitError, Qn as CdkdError, Qt as WorkGraph, R as collectDeclaredOutputNames, Rn as uploadCfnTemplate, Rt as s3BucketWebsiteUrl, S as createPreDeleteFinalSnapshot, Sn as synthesisStatusMessage, Sr as isMarkedNonRetryable, St as STATE_SOURCED_READBACK_RULES, T as unsupportedFinalSnapshotError, Tn as resolveApp, Tr as markNonRetryable, Tt as dynamicReferenceTokens, U as stateKeySecretExposure, Un as AssemblyReader, Ut as withRetry, V as isExportAliasCollision, Vn as canonicalizeRegion, Vt as INTRINSIC_KEYS, W as IAMRoleProvider, Wn as processStackMessages, Wt as DagBuilder, X as beginCommandInterruptScope, Xn as setAwsClients, Xt as AssetPublisher, Y as findSilentDropProperties, Yn as resetAwsClients, Yt as shouldRetainResource, Z as endCommandInterruptScope, Zt as stringifyValue, _ as computeImplicitDeleteEdges, _n as runDockerForeground, _t as replayWarn, a as DeploymentEventsStore, an as AssetModeResolver, ar as LocalMigrateError, at as carriesDynamicReference, b as buildFinalSnapshotIdentifier, bn as getDockerImageBySourceHash, br as normalizeAwsError, bt as requireConfigString, c as replayFailedOperations, cn as getBootstrapMarkerKey, cr as MissingCdkCliError, ct as refStateLookupFromResource, d as updatePartialReason, dn as validateAssetBucketName, dr as ProvisioningError, dt as resolveExplicitPhysicalId, en as createAssetRedirectResolver, er as CrossAccountSecretRefusalError, et as CloudControlProvider, f as UNSPECIFIED_SKIP_REASON, fn as validateContainerRepoName, fr as ResourceTimeoutError, ft as assertRegionMatch, g as IMPLICIT_DELETE_DEPENDENCIES, gn as getDockerCmd, gt as readConfigString, h as maskingRetryLogger, hn as formatDockerLoginError, hr as StackTerminationProtectionError, ht as configStringRefusal, i as DeploymentEventsReader, in as stripControlChars, ir as LocalInvokeBuildError$1, it as IntrinsicFunctionResolver, j as formatResourceLine, jn as resolveUseCdkBootstrapAssets, jt as scrubResourceRecord, k as isStatefulRecreateTargetSync, kn as resolveStateBucketWithDefault, kt as maskSecretsInText, l as replayRollback, ln as parseBootstrapMarker, lr as NestedStackChildDirectDestroyError, lt as WAFv2WebACLProvider, m as withResourceDeadline, mn as buildDockerImage, mr as StackHasActiveImportsError, mt as configBooleanRefusal, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as rewriteTemplateAssetReferences, nr as DeployCancelledError, nt as disableInstanceApiTermination, o as planFailedOps, on as BOOTSTRAP_MARKER_PREFIX, or as LocalStartServiceError, ot as cfnRefValueFromPhysicalId, p as deleteSkipReason, pn as buildDenyExternalAccessPolicy, pr as ResourceUpdateNotSupportedError, pt as coerceCfnBoolean, q as ProviderRegistry, qn as AwsClients, qt as S3StateBackend, r as DeployEngine, rn as escapeRegExp, rr as DynamicReferenceRegionAmbiguousError, rt as isTerminationProtectionPropagationError, s as planRollback, sn as ensureAssetStorage, st as getAccountInfo, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as loadPublishableAssetManifest, tt as slowCcOperationTimeoutMs, u as updatePartialMessage, un as readBootstrapMarkerBody, ur as PartialFailureError, ut as normalizeAwsTagsToCfn, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as runDockerStreaming, vt as requireConfigArray, w as refusesFinalSnapshot, wn as getLegacyStateBucketName, wr as isThrottlingError, wt as createSecretMasker, x as ccRoutedFinalSnapshotError, xn as Synthesizer, xr as withErrorHandling, xt as STATE_SOURCED_CROSS_GENERATION_RULES, y as PRE_DELETE_SNAPSHOT_TYPES, yn as AssetManifestLoader, yt as requireConfigObject, z as collectPublishedOutputNames, zn as expectedOwnerParam, zt as applyRoleArnIfSet } from "./deploy-engine-C62UGXid.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as getCurrentStackName, 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-bJ8nim2C.js";
4
+ import { t as ASGProvider } from "./asg-provider-BbWubpjP.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";
@@ -62709,28 +62709,6 @@ function redactUnaccountedOutputs(positioned, stored, accountedKeys, secrets) {
62709
62709
  return repaired ?? positioned;
62710
62710
  }
62711
62711
  /**
62712
- * The producer regions that are genuinely FOREIGN to this stack, canonicalized
62713
- * and de-duplicated (issue #2109 review).
62714
- *
62715
- * The same filter {@link classifyReplaySecretRegion} applies to its own
62716
- * `importedProducerRegions` argument, spelled here because the
62717
- * unclassifiable-reference guard needs the answer WITHOUT an expression to hand
62718
- * to the classifier. A same-region cross-stack read is the common case and
62719
- * raises no cross-region question, so it must not arm a refusal.
62720
- */
62721
- function foreignRegionsOf(producerRegions, stackRegion) {
62722
- const seen = /* @__PURE__ */ new Set();
62723
- const foreign = [];
62724
- for (const candidate of producerRegions) {
62725
- const canonical = canonicalizeRegion(candidate);
62726
- if (!canonical || canonical === canonicalizeRegion(stackRegion)) continue;
62727
- if (seen.has(canonical)) continue;
62728
- seen.add(canonical);
62729
- foreign.push(candidate);
62730
- }
62731
- return foreign;
62732
- }
62733
- /**
62734
62712
  * The `cdkd scrub` resolvers: the stack's own, plus one pinned sibling per
62735
62713
  * FOREIGN region an ARN-named secret reference asks for (issue
62736
62714
  * [#2109](https://github.com/go-to-k/cdkd/issues/2109)).
@@ -62822,7 +62800,7 @@ const DYNAMIC_REFERENCE_OPENING = "{{resolve:";
62822
62800
  * followed by a service whose VALUE CloudFormation defines as a secret. Counted
62823
62801
  * against the number of WHOLE tokens of the same class the scan matched, which
62824
62802
  * is how an ASSEMBLED reference is detected without parsing intrinsics — see
62825
- * {@link unclassifiableScrubSecretError}.
62803
+ * {@link isAssembledSecretReference}.
62826
62804
  *
62827
62805
  * WHY THE SERVICE IS PART OF THE OPENING (issue #2109 review). Counting the bare
62828
62806
  * `{{resolve:` made the guard fire on any leaf that merely MENTIONS the syntax
@@ -62859,7 +62837,7 @@ const SECRET_REFERENCE_OPENINGS = [
62859
62837
  ];
62860
62838
  /**
62861
62839
  * What an `Fn::Sub` placeholder opens with. A whole token that CONTAINS one is
62862
- * the third assembled shape — see {@link unclassifiableScrubSecretError}.
62840
+ * the third assembled shape — see {@link isAssembledSecretReference}.
62863
62841
  */
62864
62842
  const SUB_PLACEHOLDER_OPENING = "${";
62865
62843
  /** How many SECRET references {@link SECRET_REFERENCE_OPENINGS} `leaf` opens. */
@@ -62873,16 +62851,17 @@ function isSecretReferenceToken(token) {
62873
62851
  return SECRET_REFERENCE_OPENINGS.some((opening) => token.startsWith(opening));
62874
62852
  }
62875
62853
  /**
62876
- * The refusal an ASSEMBLED reference raises (issue #2109 review): a leaf that
62877
- * OPENS more SECRET references than the scan found COMPLETE tokens of that
62878
- * class in it, or a whole token that still carries an `Fn::Sub` placeholder.
62854
+ * Does this leaf ASSEMBLE a secret reference out of parts (issue #2109 review)
62855
+ * i.e. does it OPEN more SECRET references than the scan found COMPLETE
62856
+ * tokens of that class in it, or carry a whole token that still holds an
62857
+ * `Fn::Sub` placeholder?
62879
62858
  *
62880
62859
  * WHY THIS EXISTS, and it is the load-bearing half of this pre-pass rather than
62881
62860
  * an edge case. The region split runs on the RAW template leaf, BEFORE intrinsic
62882
62861
  * resolution, and the shared token scan is `\{\{resolve:[^}]+\}\}` — a class
62883
62862
  * that cannot cross a `}`. So none of the three shapes that assemble a
62884
62863
  * reference out of parts yields the ONE WHOLE token a literal reference does,
62885
- * and the third one is why this guard needs two tests rather than one
62864
+ * and the third one is why this predicate needs two tests rather than one
62886
62865
  * (all four rows MEASURED):
62887
62866
  *
62888
62867
  * ```text
@@ -62900,40 +62879,42 @@ function isSecretReferenceToken(token) {
62900
62879
  * test: a WHOLE token that still contains `${` has not been assembled yet.
62901
62880
  * Neither a Secrets Manager secret name nor an SSM parameter name may contain
62902
62881
  * `$` or `{`, so the only false positive that test can produce is a JSON key
62903
- * literally spelled `${...}` in a leaf that is NOT under an `Fn::Sub` — and
62904
- * this guard is the fail-closed side of a command whose subject is a leaked
62905
- * plaintext.
62906
- *
62907
- * With no token there is nothing to classify: {@link pinCrossRegionSecrets}
62908
- * returns the leaf BY IDENTITY, and `resolveJoin` / `resolveSub` then call
62909
- * `resolveDynamicReferences` on the PRIMARY resolver once they have joined the
62910
- * parts — issue #2109 verbatim, with the `ambiguous` refusal never firing. The
62911
- * trailing-placeholder row got there a different way before this guard's second
62912
- * test: the token WAS classified, and only downstream luck kept it safe (the
62913
- * name form then classifies `ambiguous`, the ARN form fails into
62914
- * `SCRUB_CROSS_REGION_SECRET_UNRESOLVED` because the id it looks up still has a
62915
- * literal `${Field}` in it). Both are pinned by the suite rather than assumed.
62916
- * Fixing all of this inside the resolver (classify AFTER assembly) is the
62917
- * structurally right answer and is NOT what this does: the resolver's
62918
- * dynamic-reference path is shared with deploy and is out of this change's
62919
- * scope. This guard buys the same SAFETY propertya reference whose region
62920
- * cannot be established is never resolved in the stack's own region — by
62921
- * refusing instead.
62922
- *
62923
- * GATED ON A FOREIGN PRODUCER REGION BEING ON RECORD, which is what keeps it
62924
- * from refusing the world. With no foreign region on record there is no
62925
- * cross-region question to get wrong: every reference in the stack resolves
62926
- * locally, which is what an assembled one would have done anyway, so proceeding
62927
- * is exactly today's behavior. Two residuals, stated rather than hidden:
62928
- * an assembled reference that names a FOREIGN ARN inside an `Fn::Sub` in a
62929
- * stack with NO cross-stack read on record is still resolved locally; and an
62930
- * `Fn::Join` that splits BEFORE the service name is not counted at all (see
62931
- * {@link SECRET_REFERENCE_OPENINGS}). Closing either needs the resolver-side
62932
- * classification above issue
62933
- * [#2134](https://github.com/go-to-k/cdkd/issues/2134).
62934
- */
62935
- function unclassifiableScrubSecretError(origin, leafPath, stackName, foreignProducerRegions, stackRegion) {
62936
- return new ScrubRefusalError(`Scrub of ${stackName} cannot classify a secret reference in ${origin}${leafPath ? ` at '${leafPath}'` : ""}: the value opens a '{{resolve:...}}' reference that is ASSEMBLED by an intrinsic (an Fn::Sub placeholder inside it, or an Fn::Join that splits it across parts), so the reference does not exist as a complete expression until AFTER it is resolved — and its region therefore cannot be determined before resolution. This stack read across a region boundary (producer region(s) on record: ${foreignProducerRegions.join(", ")}), so resolving it in '${stackRegion}' may look for a different region's secret: scrub would then miss the plaintext it exists to remove and report the stack clean. Refusing instead. Spell the reference as one complete literal '{{resolve:secretsmanager:<full ARN>:SecretString:...}}' — an ARN names its own region — then re-run 'cdkd scrub'.`, "SCRUB_SECRET_REFERENCE_UNCLASSIFIABLE");
62882
+ * literally spelled `${...}` in a leaf that is NOT under an `Fn::Sub` — and a
62883
+ * false positive here now costs a DEFERRAL rather than a refusal, so it is
62884
+ * cheaper than it was when this predicate raised
62885
+ * `SCRUB_SECRET_REFERENCE_UNCLASSIFIABLE`.
62886
+ *
62887
+ * WHAT A `true` MEANS FOR THE CALLER, and it changed with issue
62888
+ * [#2157](https://github.com/go-to-k/cdkd/issues/2157): the leaf is handed on
62889
+ * BY IDENTITY, so the PRIMARY resolver classifies the reference once
62890
+ * `resolveSub` / `resolveJoin` have assembled it. Before issue
62891
+ * [#2134](https://github.com/go-to-k/cdkd/issues/2134) that was issue #2109
62892
+ * verbatim the resolver had no region question at all, so an identity return
62893
+ * meant a foreign secret fetched against the stack's own endpoint, and this
62894
+ * predicate therefore had to REFUSE (gated on a foreign producer region being
62895
+ * on record, to keep it from refusing the world). #2134 put the classification
62896
+ * in `resolveDynamicReferences`, which sees the COMPLETE expression, so the
62897
+ * safety property the refusal bought a reference whose region cannot be
62898
+ * established is never resolved in the stack's own regionis now bought
62899
+ * downstream over strictly more information.
62900
+ *
62901
+ * The trailing-placeholder row reached the resolver a different way before that
62902
+ * second test existed: the truncated token WAS classified here, and only
62903
+ * downstream luck kept it safe (the name form classifies `ambiguous`, the ARN
62904
+ * form fails into `SCRUB_CROSS_REGION_SECRET_UNRESOLVED` because the id it
62905
+ * looks up still has a literal `${Field}` in it). Post-#2157 it is deferred
62906
+ * like the other three, and the resolver answers it on the assembled
62907
+ * expression instead of on a token one brace short. Every row is pinned by the
62908
+ * suite rather than assumed.
62909
+ *
62910
+ * Residual, unchanged and stated rather than hidden: an `Fn::Join` that splits
62911
+ * BEFORE the service name opens no counted reference at all (see
62912
+ * {@link SECRET_REFERENCE_OPENINGS}), so this predicate returns `false` for it
62913
+ * and the leaf is returned by identity anyway — the same destination, reached
62914
+ * without being noticed.
62915
+ */
62916
+ function isAssembledSecretReference(leaf, secretTokens) {
62917
+ return countSecretReferenceOpenings(leaf) !== secretTokens.length || secretTokens.some((token) => token.includes(SUB_PLACEHOLDER_OPENING));
62937
62918
  }
62938
62919
  /**
62939
62920
  * Resolve every FOREIGN-region `{{resolve:...}}` reference in one leaf through
@@ -62955,10 +62936,9 @@ function unclassifiableScrubSecretError(origin, leafPath, stackName, foreignProd
62955
62936
  * closed: a stopped scrub is recoverable, a lookup for a secret id spliced
62956
62937
  * together out of a plaintext is not.
62957
62938
  */
62958
- async function resolveForeignRegionTokens(leaf, stackName, ctx, leafPath) {
62939
+ async function resolveForeignRegionTokens(leaf, stackName, ctx) {
62959
62940
  const tokens = dynamicReferenceTokens(leaf);
62960
- const secretTokens = tokens.filter(isSecretReferenceToken);
62961
- if ((countSecretReferenceOpenings(leaf) !== secretTokens.length || secretTokens.some((token) => token.includes(SUB_PLACEHOLDER_OPENING))) && ctx.foreignProducerRegions.length > 0) throw unclassifiableScrubSecretError(ctx.origin, leafPath, stackName, ctx.foreignProducerRegions, ctx.stackRegion);
62941
+ if (isAssembledSecretReference(leaf, tokens.filter(isSecretReferenceToken))) return leaf;
62962
62942
  const verdicts = tokens.map((token) => [token, classifyReplaySecretRegion(token, ctx.stackRegion, ctx.producerRegions)]);
62963
62943
  for (const [, verdict] of verdicts) if (verdict.kind === "ambiguous") throw regionAmbiguousScrubSecretError(ctx.origin, stackName, verdict.secretName, verdict.foreignProducerRegions, ctx.stackRegion);
62964
62944
  if (!verdicts.some(([, verdict]) => verdict.kind === "named-region")) return leaf;
@@ -63026,16 +63006,16 @@ async function resolveForeignRegionTokens(leaf, stackName, ctx, leafPath) {
63026
63006
  * off it.
63027
63007
  */
63028
63008
  async function pinCrossRegionSecrets(bag, stackName, ctx) {
63029
- const walk = async (v, path) => {
63009
+ const walk = async (v) => {
63030
63010
  if (typeof v === "string") {
63031
63011
  if (!v.includes(DYNAMIC_REFERENCE_OPENING)) return v;
63032
- return await resolveForeignRegionTokens(v, stackName, ctx, path);
63012
+ return await resolveForeignRegionTokens(v, stackName, ctx);
63033
63013
  }
63034
63014
  if (Array.isArray(v)) {
63035
63015
  const out = new Array(v.length);
63036
63016
  let changed = false;
63037
63017
  for (let i = 0; i < v.length; i++) {
63038
- out[i] = await walk(v[i], `${path}[${i}]`);
63018
+ out[i] = await walk(v[i]);
63039
63019
  if (out[i] !== v[i]) changed = true;
63040
63020
  }
63041
63021
  return changed ? out : v;
@@ -63044,14 +63024,14 @@ async function pinCrossRegionSecrets(bag, stackName, ctx) {
63044
63024
  const out = Object.create(null);
63045
63025
  let changed = false;
63046
63026
  for (const [k, val] of Object.entries(v)) {
63047
- out[k] = await walk(val, path ? `${path}.${k}` : k);
63027
+ out[k] = await walk(val);
63048
63028
  if (out[k] !== val) changed = true;
63049
63029
  }
63050
63030
  return changed ? out : v;
63051
63031
  }
63052
63032
  return v;
63053
63033
  };
63054
- return await walk(bag, "");
63034
+ return await walk(bag);
63055
63035
  }
63056
63036
  /**
63057
63037
  * Order scrub targets PRODUCER-BEFORE-CONSUMER (issue
@@ -63460,8 +63440,11 @@ function storedValueCarriesNoPlaintext(stored) {
63460
63440
  * {@link buildExportOwnerIndex} and `producerTemplates` are keyed by stack
63461
63441
  * NAME alone, so the walk reads that name's template regardless of `Region`;
63462
63442
  * - a secret reachable only through the TRUE branch of an `Fn::If` in a
63463
- * producer's output: see {@link collectReExportHops}, which takes the FALSE
63464
- * branch for a condition it cannot evaluate.
63443
+ * producer's output: see {@link selectTakenConditionalBranches}, which takes
63444
+ * the FALSE branch for a condition it cannot evaluate -- and which, since
63445
+ * issue [#2150](https://github.com/go-to-k/cdkd/issues/2150), feeds BOTH the
63446
+ * literal scan below and {@link collectReExportHops} from that one
63447
+ * selection.
63465
63448
  */
63466
63449
  function producerPublishesSecretExpression(templates, exportOwners, producerStack, key) {
63467
63450
  const seen = /* @__PURE__ */ new Set([`${producerStack}\u0000${key}`]);
@@ -63489,7 +63472,8 @@ function producerPublishesSecretExpression(templates, exportOwners, producerStac
63489
63472
  widened = true;
63490
63473
  subjects = Object.values(outputs).map((o) => o?.Value);
63491
63474
  } else continue;
63492
- for (const subject of subjects) {
63475
+ for (const rawSubject of subjects) {
63476
+ const subject = selectTakenConditionalBranches(rawSubject);
63493
63477
  if (JSON.stringify(subject ?? null).includes("{{resolve:")) {
63494
63478
  if (widened) return {
63495
63479
  kind: "widened",
@@ -63518,6 +63502,64 @@ function producerPublishesSecretExpression(templates, exportOwners, producerStac
63518
63502
  return NO_SECRET_EXPRESSION;
63519
63503
  }
63520
63504
  /**
63505
+ * One node with every `Fn::If` collapsed to the branch scrub would TAKE
63506
+ * (issues #2146 and [#2150](https://github.com/go-to-k/cdkd/issues/2150)).
63507
+ *
63508
+ * ONE SPELLING, consumed verbatim by both halves of "does this producer publish
63509
+ * a secret expression?" — {@link collectReExportHops}, which follows the value
63510
+ * onward, and {@link producerPublishesSecretExpression}'s literal
63511
+ * `{{resolve:` scan, which tests the value itself. Those two are one question
63512
+ * asked twice, and they DISAGREED between #2146 and #2150: the hop walk took
63513
+ * the false branch while the scan `JSON.stringify`d the whole node and saw
63514
+ * both. A producer whose output is
63515
+ * `{"Fn::If": ["IsProd", "{{resolve:...}}", "plain"]}` deployed with `IsProd`
63516
+ * false therefore verdicted `declared`, the discriminator read the deployed
63517
+ * value `plain`, and scrub refused with `SCRUB_CROSS_STACK_PRODUCER_PLAINTEXT`
63518
+ * — an UNCLEARABLE refusal, because no `cdkd scrub` of the producer can turn
63519
+ * `plain` into an expression and there is no bypass flag, so every other secret
63520
+ * in the consumer stack was stranded. A second spelling of the selection would
63521
+ * only move where the two can drift apart again, so there is exactly one.
63522
+ *
63523
+ * WHICH BRANCH: the FALSE one, unconditionally, mirroring `resolveIf`'s
63524
+ * unknown-condition arm. Every condition met on this path belongs to ANOTHER
63525
+ * stack, and scrub evaluates conditions only for the stack it is scrubbing,
63526
+ * from that template's parameter DEFAULTS (it takes no `--parameters`), so a
63527
+ * producer's condition is not evaluable here.
63528
+ *
63529
+ * TWO SHAPES COLLAPSE MORE THAN THE SELECTED BRANCH, both fail-OPEN, both
63530
+ * stated rather than implied because the old whole-node `JSON.stringify` saw
63531
+ * them and this does not. Neither is CloudFormation-emittable, which is why
63532
+ * they are accepted rather than fixed:
63533
+ *
63534
+ * - a MALFORMED `Fn::If` (args not a 3-tuple) collapses to `null` -- NO branch.
63535
+ * Which branch is live is unknowable, so neither a hop nor a `{{resolve:`
63536
+ * sighting may be claimed from it. CloudFormation refuses such a template, so
63537
+ * a producer that reads this way cannot have been deployed carrying one.
63538
+ * - an object carrying `Fn::If` ALONGSIDE sibling keys keeps only the selected
63539
+ * branch, so a `{{resolve:` under a sibling is not seen. An intrinsic node has
63540
+ * exactly one key in any template CloudFormation accepts, and the hop walk
63541
+ * dropped those siblings before this function existed (its `Fn::If` arm
63542
+ * `return`ed rather than falling through), so this preserves that behaviour
63543
+ * rather than introducing it.
63544
+ *
63545
+ * Residual, and it is the direction this file accepts elsewhere: a secret
63546
+ * reachable only through the TRUE branch is not detected, so that reference
63547
+ * keeps its pre-#2133 outcome. The hop walk already took this trade for the
63548
+ * chain it follows; #2150 extends it to the scan so that the two agree.
63549
+ */
63550
+ function selectTakenConditionalBranches(node) {
63551
+ if (Array.isArray(node)) return node.map(selectTakenConditionalBranches);
63552
+ if (node === null || typeof node !== "object") return node;
63553
+ const obj = node;
63554
+ if ("Fn::If" in obj) {
63555
+ const args = obj["Fn::If"];
63556
+ return Array.isArray(args) && args.length === 3 ? selectTakenConditionalBranches(args[2]) : null;
63557
+ }
63558
+ const out = Object.create(null);
63559
+ for (const [key, child] of Object.entries(obj)) out[key] = selectTakenConditionalBranches(child);
63560
+ return out;
63561
+ }
63562
+ /**
63521
63563
  * The re-export hops one output VALUE carries (issue #2146).
63522
63564
  *
63523
63565
  * Only STATICALLY resolvable references produce a hop, mirroring
@@ -63536,8 +63578,9 @@ function producerPublishesSecretExpression(templates, exportOwners, producerStac
63536
63578
  * routinely wrapped — `Fn::Join`ing an imported password into a URL is the
63537
63579
  * shape the live fixture uses.
63538
63580
  *
63539
- * `Fn::If` IS NOT WALKED ON BOTH ARMS, and this is load-bearing rather than
63540
- * tidy (issue #2146 review). `makeCrossStackPrePass` deliberately mirrors
63581
+ * `Fn::If` NEVER REACHES THIS WALK, and the selection that used to live here
63582
+ * is load-bearing rather than tidy (issue #2146 review). `makeCrossStackPrePass`
63583
+ * deliberately mirrors
63541
63584
  * `resolveIf` — selected branch only — so that a conditional import of a
63542
63585
  * not-yet-deployed producer cannot refuse a whole stack, unbypassably, over a
63543
63586
  * reference the deploy never read. A hop walk that descended into both arms
@@ -63549,24 +63592,24 @@ function producerPublishesSecretExpression(templates, exportOwners, producerStac
63549
63592
  * flag. On main the same input answered `no`, so it would have been a
63550
63593
  * regression this change introduced.
63551
63594
  *
63552
- * WHICH branch: the FALSE one, unconditionally. `resolveIf` selects FALSE for a
63553
- * condition it cannot evaluate, and every condition here belongs to ANOTHER
63554
- * stack — scrub evaluates conditions for the stack it is scrubbing, from that
63555
- * template's parameter DEFAULTS, and has neither the parameters nor the
63556
- * evaluation for a producer's. A MALFORMED `Fn::If` (args not a 3-tuple) yields
63557
- * NO hop at all: which branch is live is unknowable, and the pre-pass's answer
63558
- * there (walk it with refusals DISARMED) has no counterpart in a function whose
63559
- * only output is "refuse or not".
63560
- *
63561
- * Residual, and it is the direction this file accepts elsewhere: a secret
63562
- * reachable only through the TRUE branch is not detected, so that reference
63563
- * keeps its pre-#2133 outcome. The asymmetry with the `{{resolve:` scan in
63564
- * {@link producerPublishesSecretExpression} which `JSON.stringify`s the whole
63565
- * subject and therefore sees BOTH arms is #2133's behaviour, unchanged here
63566
- * on purpose: narrowing it would change which stacks that older refusal fires
63567
- * on, which is not this change's subject.
63568
- */
63569
- function collectReExportHops(value, templates, exportOwners) {
63595
+ * `selectedValue` IS THE CONTRACT, and it is why this function carries no
63596
+ * `Fn::If` handling of its own: the only caller,
63597
+ * {@link producerPublishesSecretExpression}, applies
63598
+ * {@link selectTakenConditionalBranches} ONCE and hands the result to BOTH the
63599
+ * literal `{{resolve:` scan and this walk, so a node reaching here holds no
63600
+ * `Fn::If` at all. Issue [#2150](https://github.com/go-to-k/cdkd/issues/2150)
63601
+ * collapsed the two to one site deliberately: the scan used to `JSON.stringify`
63602
+ * the whole subject and therefore see BOTH arms while this walk took only the
63603
+ * false one, and two spellings of one question are exactly how they came to
63604
+ * disagree. A second selection HERE would restore that, and be worse than
63605
+ * useless measured on the #2150 lane, a per-site copy was invisible to every
63606
+ * test in the suite, because the caller had already pruned, so deleting it
63607
+ * changed nothing and no probe could fence it.
63608
+ * {@link selectTakenConditionalBranches}'s own doc carries which branch is
63609
+ * selected, what a malformed `Fn::If` yields, and the TRUE-branch residual both
63610
+ * halves accept.
63611
+ */
63612
+ function collectReExportHops(selectedValue, templates, exportOwners) {
63570
63613
  const hops = [];
63571
63614
  const walk = (node) => {
63572
63615
  if (Array.isArray(node)) {
@@ -63575,11 +63618,6 @@ function collectReExportHops(value, templates, exportOwners) {
63575
63618
  }
63576
63619
  if (node === null || typeof node !== "object") return;
63577
63620
  const obj = node;
63578
- if ("Fn::If" in obj) {
63579
- const args = obj["Fn::If"];
63580
- if (Array.isArray(args) && args.length === 3) walk(args[2]);
63581
- return;
63582
- }
63583
63621
  const imported = obj["Fn::ImportValue"];
63584
63622
  if (typeof imported === "string") for (const owner of exportOwners.get(imported) ?? []) hops.push({
63585
63623
  stack: owner,
@@ -63597,7 +63635,7 @@ function collectReExportHops(value, templates, exportOwners) {
63597
63635
  }
63598
63636
  for (const child of Object.values(obj)) walk(child);
63599
63637
  };
63600
- walk(value);
63638
+ walk(selectedValue);
63601
63639
  return hops;
63602
63640
  }
63603
63641
  /**
@@ -63637,6 +63675,44 @@ function buildExportOwnerIndex(templates) {
63637
63675
  return owners;
63638
63676
  }
63639
63677
  /**
63678
+ * Is this the issue #2134 refusal -- a `{{resolve:...}}` reference whose region
63679
+ * cdkd cannot establish?
63680
+ *
63681
+ * WHY IT IS NOT BEST-EFFORT, which is the whole reason this predicate exists.
63682
+ * `scrubStack` wraps each resolution pass in a `catch { logger.debug }` so one
63683
+ * unresolvable resource does not abandon the rest of the scrub. Every other
63684
+ * error reaching those catches means "this resource resolved partially, carry
63685
+ * on". This one means cdkd could not tell which region owns a secret reference,
63686
+ * so NO NEEDLE was recorded for it and the plaintext is still in `state.json` --
63687
+ * and swallowed, the command then reports `No plaintext secrets found` and
63688
+ * exits 0 over exactly the state it exists to clean. That is why the pre-pass
63689
+ * refusals are placed OUTSIDE those catches; this one is raised from inside
63690
+ * `resolveDynamicReferences`, so it cannot be placed and has to be re-raised.
63691
+ *
63692
+ * By CAUSE CHAIN rather than a bare `instanceof`, for the reason
63693
+ * {@link isByDesignRefusal} states: the refusal reaches those catches through
63694
+ * `resolver.resolve`, which wraps in several places, so a top-link-only test
63695
+ * silently swallows a wrapped one.
63696
+ *
63697
+ * TWO SITES IN THIS CALL GRAPH STILL SWALLOW IT, named rather than left to be
63698
+ * rediscovered (issue #2134 review round 2):
63699
+ *
63700
+ * - `evaluateConditions` (`intrinsic-function-resolver.ts`) warns and degrades
63701
+ * the condition to `false`. Accepted for now: a condition evaluates to a
63702
+ * boolean and carries no secret INTO state, and this file's own note above
63703
+ * `resolveCrossStackReads` records why the conditions pass is deliberately
63704
+ * not refusal-armed -- it runs BEFORE scrub knows which branch the deploy
63705
+ * took, so refusing there can reject a whole stack over a branch never taken.
63706
+ * The residual is that the FALSE branch is then selected, which that note
63707
+ * already states.
63708
+ * - the `!nodeCanRefuse` arm of the cross-stack pre-pass, which debugs and
63709
+ * returns. Defensible: a SUPPRESSED output writes no state key, so there is
63710
+ * no persisted plaintext for a missing needle to leave behind.
63711
+ */
63712
+ function isRegionAmbiguousRefusal(err) {
63713
+ return err instanceof Error && errorCauseChain(err).some((link) => link instanceof DynamicReferenceRegionAmbiguousError);
63714
+ }
63715
+ /**
63640
63716
  * Build the CROSS-STACK pre-pass for ONE stack's scrub (issue
63641
63717
  * [#2133](https://github.com/go-to-k/cdkd/issues/2133)).
63642
63718
  *
@@ -63952,7 +64028,6 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
63952
64028
  const resolvers = new ScrubResolvers(region);
63953
64029
  const resolver = resolvers.primary;
63954
64030
  const producerRegions = producerRegionsFromState(state);
63955
- const foreignProducerRegions = foreignRegionsOf(producerRegions, region);
63956
64031
  let parameters = {};
63957
64032
  let conditions = {};
63958
64033
  try {
@@ -63969,6 +64044,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
63969
64044
  stackName: stack.stackName,
63970
64045
  stateBackend: resolverStateBackend,
63971
64046
  ...recordedSecretValues && { recordedSecretValues },
64047
+ producerRegions,
63972
64048
  bestEffort: true
63973
64049
  });
63974
64050
  const resolveCrossStackReads = makeCrossStackPrePass({
@@ -63992,7 +64068,6 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
63992
64068
  const resolveInput = await pinCrossRegionSecrets(templateResource.Properties, stack.stackName, {
63993
64069
  stackRegion: region,
63994
64070
  producerRegions,
63995
- foreignProducerRegions,
63996
64071
  resolvers,
63997
64072
  recordedSecretValues,
63998
64073
  origin: `resource '${logicalId}'`
@@ -64002,6 +64077,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
64002
64077
  try {
64003
64078
  await resolver.resolve(resolveInput, resourceContext);
64004
64079
  } catch (err) {
64080
+ if (isRegionAmbiguousRefusal(err)) throw err;
64005
64081
  logger.debug(`Resolution of ${logicalId} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), recordedSecretValues)}`);
64006
64082
  }
64007
64083
  perResourceTemplateProps.set(logicalId, templateResource.Properties);
@@ -64019,7 +64095,6 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
64019
64095
  const nameSource = await pinCrossRegionSecrets(declaredExportName, stack.stackName, {
64020
64096
  stackRegion: region,
64021
64097
  producerRegions,
64022
- foreignProducerRegions,
64023
64098
  resolvers,
64024
64099
  recordedSecretValues: outputSecrets,
64025
64100
  origin: `Export.Name of output '${name}'`
@@ -64029,6 +64104,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
64029
64104
  try {
64030
64105
  exportName = await resolver.resolve(nameSource, nameContext);
64031
64106
  } catch (err) {
64107
+ if (isRegionAmbiguousRefusal(err)) throw err;
64032
64108
  outputsSourceUntrusted = true;
64033
64109
  logger.warn(`Export.Name of output ${name} could not be resolved during scrub (${maskSecretsInText(err instanceof Error ? err.message : String(err), outputSecrets)}) — 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.`);
64034
64110
  }
@@ -64053,7 +64129,6 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
64053
64129
  const valueSource = await pinCrossRegionSecrets(value, stack.stackName, {
64054
64130
  stackRegion: region,
64055
64131
  producerRegions,
64056
- foreignProducerRegions,
64057
64132
  resolvers,
64058
64133
  recordedSecretValues: outputSecrets,
64059
64134
  origin: `output '${name}'`
@@ -64063,6 +64138,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
64063
64138
  try {
64064
64139
  await resolver.resolve(valueSource, valueContext);
64065
64140
  } catch (err) {
64141
+ if (isRegionAmbiguousRefusal(err)) throw err;
64066
64142
  logger.debug(`Resolution of output ${name} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), outputSecrets)}`);
64067
64143
  }
64068
64144
  }
@@ -86049,7 +86125,7 @@ function createMigrateCommand() {
86049
86125
  */
86050
86126
  function buildProgram() {
86051
86127
  const program = new Command();
86052
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.35");
86128
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.37");
86053
86129
  program.hook("preAction", (_thisCommand, actionCommand) => {
86054
86130
  const { profile } = actionCommand.optsWithGlobals();
86055
86131
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;