@go-to-k/cdkd 0.284.36 → 0.284.38

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 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-Csb2eS97.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-DyxASW-O.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-2wQUtu7f.js";
4
+ import { t as ASGProvider } from "./asg-provider-kqBWElNc.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";
@@ -58335,19 +58335,40 @@ async function runDestroyForStack(stackName, state, ctx) {
58335
58335
  const stackRegion = state.region;
58336
58336
  let destroyProviderRegistry = ctx.providerRegistry;
58337
58337
  let destroyAwsClients;
58338
+ let regionSwitched = false;
58339
+ const restoreBaseRegionAndClients = () => {
58340
+ if (!regionSwitched) return;
58341
+ regionSwitched = false;
58342
+ const switchedClients = destroyAwsClients;
58343
+ destroyAwsClients = void 0;
58344
+ process.env["AWS_REGION"] = ctx.baseRegion;
58345
+ process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
58346
+ setAwsClients(ctx.baseAwsClients);
58347
+ try {
58348
+ switchedClients?.destroy();
58349
+ } catch (destroyError) {
58350
+ logger.debug(`Failed to destroy region-scoped AWS clients: ${destroyError instanceof Error ? destroyError.message : String(destroyError)}`);
58351
+ }
58352
+ };
58338
58353
  if (stackRegion && stackRegion !== ctx.baseRegion) {
58339
58354
  logger.info(`Stack region: ${stackRegion}`);
58340
58355
  process.env["AWS_REGION"] = stackRegion;
58341
58356
  process.env["AWS_DEFAULT_REGION"] = stackRegion;
58342
- destroyAwsClients = new AwsClients({
58343
- region: stackRegion,
58344
- ...ctx.profile && { profile: ctx.profile }
58345
- });
58346
- setAwsClients(destroyAwsClients);
58347
- destroyProviderRegistry = new ProviderRegistry();
58348
- registerAllProviders(destroyProviderRegistry);
58349
- destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
58350
- if (ctx.allowUnsupportedTypes?.length) destroyProviderRegistry.allowUnsupportedTypes(ctx.allowUnsupportedTypes);
58357
+ regionSwitched = true;
58358
+ try {
58359
+ destroyAwsClients = new AwsClients({
58360
+ region: stackRegion,
58361
+ ...ctx.profile && { profile: ctx.profile }
58362
+ });
58363
+ setAwsClients(destroyAwsClients);
58364
+ destroyProviderRegistry = new ProviderRegistry();
58365
+ registerAllProviders(destroyProviderRegistry);
58366
+ destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
58367
+ if (ctx.allowUnsupportedTypes?.length) destroyProviderRegistry.allowUnsupportedTypes(ctx.allowUnsupportedTypes);
58368
+ } catch (setupError) {
58369
+ restoreBaseRegionAndClients();
58370
+ throw setupError;
58371
+ }
58351
58372
  }
58352
58373
  const renderer = getLiveRenderer();
58353
58374
  let draining = false;
@@ -58367,10 +58388,11 @@ async function runDestroyForStack(stackName, state, ctx) {
58367
58388
  };
58368
58389
  process.setMaxListeners(Math.max(process.getMaxListeners(), 100));
58369
58390
  process.on("SIGINT", sigintHandler);
58370
- logger.info(`\nAcquiring lock for stack ${stackName}...`);
58371
58391
  try {
58372
- await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
58392
+ logger.info(`\nAcquiring lock for stack ${stackName}...`);
58393
+ if (!await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy")) throw new Error(`Could not acquire lock for stack '${stackName}' (${regionForState}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${stackName} --stack-region ${regionForState}' if you are certain no other process is active.`);
58373
58394
  } catch (error) {
58395
+ restoreBaseRegionAndClients();
58374
58396
  process.removeListener("SIGINT", sigintHandler);
58375
58397
  throw error;
58376
58398
  }
@@ -58382,6 +58404,7 @@ async function runDestroyForStack(stackName, state, ctx) {
58382
58404
  } catch (releaseErr) {
58383
58405
  logger.warn(`Failed to release lock after strong-ref refusal/failure: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
58384
58406
  }
58407
+ restoreBaseRegionAndClients();
58385
58408
  process.removeListener("SIGINT", sigintHandler);
58386
58409
  };
58387
58410
  let consumers;
@@ -58422,8 +58445,8 @@ async function runDestroyForStack(stackName, state, ctx) {
58422
58445
  }
58423
58446
  });
58424
58447
  };
58425
- renderer.start();
58426
58448
  try {
58449
+ renderer.start();
58427
58450
  logger.info("Building dependency graph...");
58428
58451
  const template = {
58429
58452
  AWSTemplateFormatVersion: "2010-09-09",
@@ -58693,14 +58716,9 @@ async function runDestroyForStack(stackName, state, ctx) {
58693
58716
  await ctx.lockManager.releaseLock(stackName, regionForState);
58694
58717
  } finally {
58695
58718
  process.removeListener("SIGINT", sigintHandler);
58719
+ restoreBaseRegionAndClients();
58696
58720
  }
58697
58721
  result.interrupted ||= draining;
58698
- if (destroyAwsClients) {
58699
- destroyAwsClients.destroy();
58700
- process.env["AWS_REGION"] = ctx.baseRegion;
58701
- process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
58702
- setAwsClients(ctx.baseAwsClients);
58703
- }
58704
58722
  }
58705
58723
  return result;
58706
58724
  }
@@ -62709,28 +62727,6 @@ function redactUnaccountedOutputs(positioned, stored, accountedKeys, secrets) {
62709
62727
  return repaired ?? positioned;
62710
62728
  }
62711
62729
  /**
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
62730
  * The `cdkd scrub` resolvers: the stack's own, plus one pinned sibling per
62735
62731
  * FOREIGN region an ARN-named secret reference asks for (issue
62736
62732
  * [#2109](https://github.com/go-to-k/cdkd/issues/2109)).
@@ -62822,7 +62818,7 @@ const DYNAMIC_REFERENCE_OPENING = "{{resolve:";
62822
62818
  * followed by a service whose VALUE CloudFormation defines as a secret. Counted
62823
62819
  * against the number of WHOLE tokens of the same class the scan matched, which
62824
62820
  * is how an ASSEMBLED reference is detected without parsing intrinsics — see
62825
- * {@link unclassifiableScrubSecretError}.
62821
+ * {@link isAssembledSecretReference}.
62826
62822
  *
62827
62823
  * WHY THE SERVICE IS PART OF THE OPENING (issue #2109 review). Counting the bare
62828
62824
  * `{{resolve:` made the guard fire on any leaf that merely MENTIONS the syntax
@@ -62859,7 +62855,7 @@ const SECRET_REFERENCE_OPENINGS = [
62859
62855
  ];
62860
62856
  /**
62861
62857
  * What an `Fn::Sub` placeholder opens with. A whole token that CONTAINS one is
62862
- * the third assembled shape — see {@link unclassifiableScrubSecretError}.
62858
+ * the third assembled shape — see {@link isAssembledSecretReference}.
62863
62859
  */
62864
62860
  const SUB_PLACEHOLDER_OPENING = "${";
62865
62861
  /** How many SECRET references {@link SECRET_REFERENCE_OPENINGS} `leaf` opens. */
@@ -62873,16 +62869,17 @@ function isSecretReferenceToken(token) {
62873
62869
  return SECRET_REFERENCE_OPENINGS.some((opening) => token.startsWith(opening));
62874
62870
  }
62875
62871
  /**
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.
62872
+ * Does this leaf ASSEMBLE a secret reference out of parts (issue #2109 review)
62873
+ * i.e. does it OPEN more SECRET references than the scan found COMPLETE
62874
+ * tokens of that class in it, or carry a whole token that still holds an
62875
+ * `Fn::Sub` placeholder?
62879
62876
  *
62880
62877
  * WHY THIS EXISTS, and it is the load-bearing half of this pre-pass rather than
62881
62878
  * an edge case. The region split runs on the RAW template leaf, BEFORE intrinsic
62882
62879
  * resolution, and the shared token scan is `\{\{resolve:[^}]+\}\}` — a class
62883
62880
  * that cannot cross a `}`. So none of the three shapes that assemble a
62884
62881
  * 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
62882
+ * and the third one is why this predicate needs two tests rather than one
62886
62883
  * (all four rows MEASURED):
62887
62884
  *
62888
62885
  * ```text
@@ -62900,40 +62897,42 @@ function isSecretReferenceToken(token) {
62900
62897
  * test: a WHOLE token that still contains `${` has not been assembled yet.
62901
62898
  * Neither a Secrets Manager secret name nor an SSM parameter name may contain
62902
62899
  * `$` 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");
62900
+ * literally spelled `${...}` in a leaf that is NOT under an `Fn::Sub` — and a
62901
+ * false positive here now costs a DEFERRAL rather than a refusal, so it is
62902
+ * cheaper than it was when this predicate raised
62903
+ * `SCRUB_SECRET_REFERENCE_UNCLASSIFIABLE`.
62904
+ *
62905
+ * WHAT A `true` MEANS FOR THE CALLER, and it changed with issue
62906
+ * [#2157](https://github.com/go-to-k/cdkd/issues/2157): the leaf is handed on
62907
+ * BY IDENTITY, so the PRIMARY resolver classifies the reference once
62908
+ * `resolveSub` / `resolveJoin` have assembled it. Before issue
62909
+ * [#2134](https://github.com/go-to-k/cdkd/issues/2134) that was issue #2109
62910
+ * verbatim the resolver had no region question at all, so an identity return
62911
+ * meant a foreign secret fetched against the stack's own endpoint, and this
62912
+ * predicate therefore had to REFUSE (gated on a foreign producer region being
62913
+ * on record, to keep it from refusing the world). #2134 put the classification
62914
+ * in `resolveDynamicReferences`, which sees the COMPLETE expression, so the
62915
+ * safety property the refusal bought a reference whose region cannot be
62916
+ * established is never resolved in the stack's own regionis now bought
62917
+ * downstream over strictly more information.
62918
+ *
62919
+ * The trailing-placeholder row reached the resolver a different way before that
62920
+ * second test existed: the truncated token WAS classified here, and only
62921
+ * downstream luck kept it safe (the name form classifies `ambiguous`, the ARN
62922
+ * form fails into `SCRUB_CROSS_REGION_SECRET_UNRESOLVED` because the id it
62923
+ * looks up still has a literal `${Field}` in it). Post-#2157 it is deferred
62924
+ * like the other three, and the resolver answers it on the assembled
62925
+ * expression instead of on a token one brace short. Every row is pinned by the
62926
+ * suite rather than assumed.
62927
+ *
62928
+ * Residual, unchanged and stated rather than hidden: an `Fn::Join` that splits
62929
+ * BEFORE the service name opens no counted reference at all (see
62930
+ * {@link SECRET_REFERENCE_OPENINGS}), so this predicate returns `false` for it
62931
+ * and the leaf is returned by identity anyway — the same destination, reached
62932
+ * without being noticed.
62933
+ */
62934
+ function isAssembledSecretReference(leaf, secretTokens) {
62935
+ return countSecretReferenceOpenings(leaf) !== secretTokens.length || secretTokens.some((token) => token.includes(SUB_PLACEHOLDER_OPENING));
62937
62936
  }
62938
62937
  /**
62939
62938
  * Resolve every FOREIGN-region `{{resolve:...}}` reference in one leaf through
@@ -62955,10 +62954,9 @@ function unclassifiableScrubSecretError(origin, leafPath, stackName, foreignProd
62955
62954
  * closed: a stopped scrub is recoverable, a lookup for a secret id spliced
62956
62955
  * together out of a plaintext is not.
62957
62956
  */
62958
- async function resolveForeignRegionTokens(leaf, stackName, ctx, leafPath) {
62957
+ async function resolveForeignRegionTokens(leaf, stackName, ctx) {
62959
62958
  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);
62959
+ if (isAssembledSecretReference(leaf, tokens.filter(isSecretReferenceToken))) return leaf;
62962
62960
  const verdicts = tokens.map((token) => [token, classifyReplaySecretRegion(token, ctx.stackRegion, ctx.producerRegions)]);
62963
62961
  for (const [, verdict] of verdicts) if (verdict.kind === "ambiguous") throw regionAmbiguousScrubSecretError(ctx.origin, stackName, verdict.secretName, verdict.foreignProducerRegions, ctx.stackRegion);
62964
62962
  if (!verdicts.some(([, verdict]) => verdict.kind === "named-region")) return leaf;
@@ -63026,16 +63024,16 @@ async function resolveForeignRegionTokens(leaf, stackName, ctx, leafPath) {
63026
63024
  * off it.
63027
63025
  */
63028
63026
  async function pinCrossRegionSecrets(bag, stackName, ctx) {
63029
- const walk = async (v, path) => {
63027
+ const walk = async (v) => {
63030
63028
  if (typeof v === "string") {
63031
63029
  if (!v.includes(DYNAMIC_REFERENCE_OPENING)) return v;
63032
- return await resolveForeignRegionTokens(v, stackName, ctx, path);
63030
+ return await resolveForeignRegionTokens(v, stackName, ctx);
63033
63031
  }
63034
63032
  if (Array.isArray(v)) {
63035
63033
  const out = new Array(v.length);
63036
63034
  let changed = false;
63037
63035
  for (let i = 0; i < v.length; i++) {
63038
- out[i] = await walk(v[i], `${path}[${i}]`);
63036
+ out[i] = await walk(v[i]);
63039
63037
  if (out[i] !== v[i]) changed = true;
63040
63038
  }
63041
63039
  return changed ? out : v;
@@ -63044,14 +63042,14 @@ async function pinCrossRegionSecrets(bag, stackName, ctx) {
63044
63042
  const out = Object.create(null);
63045
63043
  let changed = false;
63046
63044
  for (const [k, val] of Object.entries(v)) {
63047
- out[k] = await walk(val, path ? `${path}.${k}` : k);
63045
+ out[k] = await walk(val);
63048
63046
  if (out[k] !== val) changed = true;
63049
63047
  }
63050
63048
  return changed ? out : v;
63051
63049
  }
63052
63050
  return v;
63053
63051
  };
63054
- return await walk(bag, "");
63052
+ return await walk(bag);
63055
63053
  }
63056
63054
  /**
63057
63055
  * Order scrub targets PRODUCER-BEFORE-CONSUMER (issue
@@ -63460,8 +63458,11 @@ function storedValueCarriesNoPlaintext(stored) {
63460
63458
  * {@link buildExportOwnerIndex} and `producerTemplates` are keyed by stack
63461
63459
  * NAME alone, so the walk reads that name's template regardless of `Region`;
63462
63460
  * - 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.
63461
+ * producer's output: see {@link selectTakenConditionalBranches}, which takes
63462
+ * the FALSE branch for a condition it cannot evaluate -- and which, since
63463
+ * issue [#2150](https://github.com/go-to-k/cdkd/issues/2150), feeds BOTH the
63464
+ * literal scan below and {@link collectReExportHops} from that one
63465
+ * selection.
63465
63466
  */
63466
63467
  function producerPublishesSecretExpression(templates, exportOwners, producerStack, key) {
63467
63468
  const seen = /* @__PURE__ */ new Set([`${producerStack}\u0000${key}`]);
@@ -63489,7 +63490,8 @@ function producerPublishesSecretExpression(templates, exportOwners, producerStac
63489
63490
  widened = true;
63490
63491
  subjects = Object.values(outputs).map((o) => o?.Value);
63491
63492
  } else continue;
63492
- for (const subject of subjects) {
63493
+ for (const rawSubject of subjects) {
63494
+ const subject = selectTakenConditionalBranches(rawSubject);
63493
63495
  if (JSON.stringify(subject ?? null).includes("{{resolve:")) {
63494
63496
  if (widened) return {
63495
63497
  kind: "widened",
@@ -63518,6 +63520,64 @@ function producerPublishesSecretExpression(templates, exportOwners, producerStac
63518
63520
  return NO_SECRET_EXPRESSION;
63519
63521
  }
63520
63522
  /**
63523
+ * One node with every `Fn::If` collapsed to the branch scrub would TAKE
63524
+ * (issues #2146 and [#2150](https://github.com/go-to-k/cdkd/issues/2150)).
63525
+ *
63526
+ * ONE SPELLING, consumed verbatim by both halves of "does this producer publish
63527
+ * a secret expression?" — {@link collectReExportHops}, which follows the value
63528
+ * onward, and {@link producerPublishesSecretExpression}'s literal
63529
+ * `{{resolve:` scan, which tests the value itself. Those two are one question
63530
+ * asked twice, and they DISAGREED between #2146 and #2150: the hop walk took
63531
+ * the false branch while the scan `JSON.stringify`d the whole node and saw
63532
+ * both. A producer whose output is
63533
+ * `{"Fn::If": ["IsProd", "{{resolve:...}}", "plain"]}` deployed with `IsProd`
63534
+ * false therefore verdicted `declared`, the discriminator read the deployed
63535
+ * value `plain`, and scrub refused with `SCRUB_CROSS_STACK_PRODUCER_PLAINTEXT`
63536
+ * — an UNCLEARABLE refusal, because no `cdkd scrub` of the producer can turn
63537
+ * `plain` into an expression and there is no bypass flag, so every other secret
63538
+ * in the consumer stack was stranded. A second spelling of the selection would
63539
+ * only move where the two can drift apart again, so there is exactly one.
63540
+ *
63541
+ * WHICH BRANCH: the FALSE one, unconditionally, mirroring `resolveIf`'s
63542
+ * unknown-condition arm. Every condition met on this path belongs to ANOTHER
63543
+ * stack, and scrub evaluates conditions only for the stack it is scrubbing,
63544
+ * from that template's parameter DEFAULTS (it takes no `--parameters`), so a
63545
+ * producer's condition is not evaluable here.
63546
+ *
63547
+ * TWO SHAPES COLLAPSE MORE THAN THE SELECTED BRANCH, both fail-OPEN, both
63548
+ * stated rather than implied because the old whole-node `JSON.stringify` saw
63549
+ * them and this does not. Neither is CloudFormation-emittable, which is why
63550
+ * they are accepted rather than fixed:
63551
+ *
63552
+ * - a MALFORMED `Fn::If` (args not a 3-tuple) collapses to `null` -- NO branch.
63553
+ * Which branch is live is unknowable, so neither a hop nor a `{{resolve:`
63554
+ * sighting may be claimed from it. CloudFormation refuses such a template, so
63555
+ * a producer that reads this way cannot have been deployed carrying one.
63556
+ * - an object carrying `Fn::If` ALONGSIDE sibling keys keeps only the selected
63557
+ * branch, so a `{{resolve:` under a sibling is not seen. An intrinsic node has
63558
+ * exactly one key in any template CloudFormation accepts, and the hop walk
63559
+ * dropped those siblings before this function existed (its `Fn::If` arm
63560
+ * `return`ed rather than falling through), so this preserves that behaviour
63561
+ * rather than introducing it.
63562
+ *
63563
+ * Residual, and it is the direction this file accepts elsewhere: a secret
63564
+ * reachable only through the TRUE branch is not detected, so that reference
63565
+ * keeps its pre-#2133 outcome. The hop walk already took this trade for the
63566
+ * chain it follows; #2150 extends it to the scan so that the two agree.
63567
+ */
63568
+ function selectTakenConditionalBranches(node) {
63569
+ if (Array.isArray(node)) return node.map(selectTakenConditionalBranches);
63570
+ if (node === null || typeof node !== "object") return node;
63571
+ const obj = node;
63572
+ if ("Fn::If" in obj) {
63573
+ const args = obj["Fn::If"];
63574
+ return Array.isArray(args) && args.length === 3 ? selectTakenConditionalBranches(args[2]) : null;
63575
+ }
63576
+ const out = Object.create(null);
63577
+ for (const [key, child] of Object.entries(obj)) out[key] = selectTakenConditionalBranches(child);
63578
+ return out;
63579
+ }
63580
+ /**
63521
63581
  * The re-export hops one output VALUE carries (issue #2146).
63522
63582
  *
63523
63583
  * Only STATICALLY resolvable references produce a hop, mirroring
@@ -63536,8 +63596,9 @@ function producerPublishesSecretExpression(templates, exportOwners, producerStac
63536
63596
  * routinely wrapped — `Fn::Join`ing an imported password into a URL is the
63537
63597
  * shape the live fixture uses.
63538
63598
  *
63539
- * `Fn::If` IS NOT WALKED ON BOTH ARMS, and this is load-bearing rather than
63540
- * tidy (issue #2146 review). `makeCrossStackPrePass` deliberately mirrors
63599
+ * `Fn::If` NEVER REACHES THIS WALK, and the selection that used to live here
63600
+ * is load-bearing rather than tidy (issue #2146 review). `makeCrossStackPrePass`
63601
+ * deliberately mirrors
63541
63602
  * `resolveIf` — selected branch only — so that a conditional import of a
63542
63603
  * not-yet-deployed producer cannot refuse a whole stack, unbypassably, over a
63543
63604
  * reference the deploy never read. A hop walk that descended into both arms
@@ -63549,24 +63610,24 @@ function producerPublishesSecretExpression(templates, exportOwners, producerStac
63549
63610
  * flag. On main the same input answered `no`, so it would have been a
63550
63611
  * regression this change introduced.
63551
63612
  *
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) {
63613
+ * `selectedValue` IS THE CONTRACT, and it is why this function carries no
63614
+ * `Fn::If` handling of its own: the only caller,
63615
+ * {@link producerPublishesSecretExpression}, applies
63616
+ * {@link selectTakenConditionalBranches} ONCE and hands the result to BOTH the
63617
+ * literal `{{resolve:` scan and this walk, so a node reaching here holds no
63618
+ * `Fn::If` at all. Issue [#2150](https://github.com/go-to-k/cdkd/issues/2150)
63619
+ * collapsed the two to one site deliberately: the scan used to `JSON.stringify`
63620
+ * the whole subject and therefore see BOTH arms while this walk took only the
63621
+ * false one, and two spellings of one question are exactly how they came to
63622
+ * disagree. A second selection HERE would restore that, and be worse than
63623
+ * useless measured on the #2150 lane, a per-site copy was invisible to every
63624
+ * test in the suite, because the caller had already pruned, so deleting it
63625
+ * changed nothing and no probe could fence it.
63626
+ * {@link selectTakenConditionalBranches}'s own doc carries which branch is
63627
+ * selected, what a malformed `Fn::If` yields, and the TRUE-branch residual both
63628
+ * halves accept.
63629
+ */
63630
+ function collectReExportHops(selectedValue, templates, exportOwners) {
63570
63631
  const hops = [];
63571
63632
  const walk = (node) => {
63572
63633
  if (Array.isArray(node)) {
@@ -63575,11 +63636,6 @@ function collectReExportHops(value, templates, exportOwners) {
63575
63636
  }
63576
63637
  if (node === null || typeof node !== "object") return;
63577
63638
  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
63639
  const imported = obj["Fn::ImportValue"];
63584
63640
  if (typeof imported === "string") for (const owner of exportOwners.get(imported) ?? []) hops.push({
63585
63641
  stack: owner,
@@ -63597,7 +63653,7 @@ function collectReExportHops(value, templates, exportOwners) {
63597
63653
  }
63598
63654
  for (const child of Object.values(obj)) walk(child);
63599
63655
  };
63600
- walk(value);
63656
+ walk(selectedValue);
63601
63657
  return hops;
63602
63658
  }
63603
63659
  /**
@@ -63990,7 +64046,6 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
63990
64046
  const resolvers = new ScrubResolvers(region);
63991
64047
  const resolver = resolvers.primary;
63992
64048
  const producerRegions = producerRegionsFromState(state);
63993
- const foreignProducerRegions = foreignRegionsOf(producerRegions, region);
63994
64049
  let parameters = {};
63995
64050
  let conditions = {};
63996
64051
  try {
@@ -64031,7 +64086,6 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
64031
64086
  const resolveInput = await pinCrossRegionSecrets(templateResource.Properties, stack.stackName, {
64032
64087
  stackRegion: region,
64033
64088
  producerRegions,
64034
- foreignProducerRegions,
64035
64089
  resolvers,
64036
64090
  recordedSecretValues,
64037
64091
  origin: `resource '${logicalId}'`
@@ -64059,7 +64113,6 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
64059
64113
  const nameSource = await pinCrossRegionSecrets(declaredExportName, stack.stackName, {
64060
64114
  stackRegion: region,
64061
64115
  producerRegions,
64062
- foreignProducerRegions,
64063
64116
  resolvers,
64064
64117
  recordedSecretValues: outputSecrets,
64065
64118
  origin: `Export.Name of output '${name}'`
@@ -64094,7 +64147,6 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
64094
64147
  const valueSource = await pinCrossRegionSecrets(value, stack.stackName, {
64095
64148
  stackRegion: region,
64096
64149
  producerRegions,
64097
- foreignProducerRegions,
64098
64150
  resolvers,
64099
64151
  recordedSecretValues: outputSecrets,
64100
64152
  origin: `output '${name}'`
@@ -66112,7 +66164,7 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
66112
66164
  skipped: () => []
66113
66165
  }));
66114
66166
  if (driftedOutcomes.length === 0) continue;
66115
- await lockManager.acquireLock(report.stackName, report.region, owner, "drift-accept");
66167
+ if (!await lockManager.acquireLock(report.stackName, report.region, owner, "drift-accept")) throw new Error(`Could not acquire lock for stack '${report.stackName}' (${report.region}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${report.stackName} --stack-region ${report.region}' if you are certain no other process is active.`);
66116
66168
  try {
66117
66169
  const resources = { ...report.state.resources };
66118
66170
  for (const outcome of driftedOutcomes) {
@@ -66736,7 +66788,7 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
66736
66788
  skipped: () => []
66737
66789
  }));
66738
66790
  if (driftedOutcomes.length === 0) continue;
66739
- await lockManager.acquireLock(report.stackName, report.region, owner, "drift-revert");
66791
+ if (!await lockManager.acquireLock(report.stackName, report.region, owner, "drift-revert")) throw new Error(`Could not acquire lock for stack '${report.stackName}' (${report.region}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${report.stackName} --stack-region ${report.region}' if you are certain no other process is active.`);
66740
66792
  const narrowedByLogicalId = /* @__PURE__ */ new Map();
66741
66793
  const revertSecretResolvers = new DriftSecretResolvers(report.region);
66742
66794
  const revertProducerRegions = producerRegionsFromState(report.state);
@@ -69026,7 +69078,7 @@ async function exportCommand(stackArg, options) {
69026
69078
  }
69027
69079
  const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
69028
69080
  if (!options.dryRun) {
69029
- if (!await lockManager.acquireLock(resolvedStackName, targetRegion, owner, "export")) throw new Error(`Could not acquire lock for stack '${resolvedStackName}' (${targetRegion}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${resolvedStackName}' if you are certain no other process is active.`);
69081
+ if (!await lockManager.acquireLock(resolvedStackName, targetRegion, owner, "export")) throw new Error(`Could not acquire lock for stack '${resolvedStackName}' (${targetRegion}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${resolvedStackName} --stack-region ${targetRegion}' if you are certain no other process is active.`);
69030
69082
  }
69031
69083
  try {
69032
69084
  if (nestedStackRows.length > 0) {
@@ -70942,7 +70994,7 @@ async function runPerStackImportLoop(args) {
70942
70994
  try {
70943
70995
  await deps.lockManager.acquireLockWithRetry(node.stackName, node.region, deps.lockOwner, "export", CHILD_LOCK_RETRY_MAX_ATTEMPTS, CHILD_LOCK_RETRY_DELAY_MS);
70944
70996
  } catch (err) {
70945
- throw new Error(`Could not acquire lock for nested-stack child '${node.stackName}' (${node.region}) — another cdkd process held it through the retry window. Wait for it to finish, or run 'cdkd force-unlock ${node.stackName}' if you are certain no other process is active. No CloudFormation changeset has been submitted; cdkd state is unchanged.`, { cause: err instanceof Error ? err : void 0 });
70997
+ throw new Error(`Could not acquire lock for nested-stack child '${node.stackName}' (${node.region}) — another cdkd process held it through the retry window. Wait for it to finish, or run 'cdkd force-unlock ${node.stackName} --stack-region ${node.region}' if you are certain no other process is active. No CloudFormation changeset has been submitted; cdkd state is unchanged.`, { cause: err instanceof Error ? err : void 0 });
70946
70998
  }
70947
70999
  acquiredChildLocks.push({
70948
71000
  stackName: node.stackName,
@@ -72483,7 +72535,7 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
72483
72535
  };
72484
72536
  }
72485
72537
  const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
72486
- await lockManager.acquireLock(stackName, region, owner, "state-refresh-observed");
72538
+ if (!await lockManager.acquireLock(stackName, region, owner, "state-refresh-observed")) throw new Error(`Could not acquire lock for stack '${stackName}' (${region}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${stackName} --stack-region ${region}' if you are certain no other process is active.`);
72487
72539
  try {
72488
72540
  let refreshed = 0;
72489
72541
  let unsupported = 0;
@@ -74862,7 +74914,9 @@ async function orphanCommand(pathArgs, options) {
74862
74914
  const targetRegion = await pickStackRegion(stateBackend, stackInfo.stackName, stackInfo.region, options.stackRegion);
74863
74915
  logger.info(`Target: ${stackInfo.stackName} (${targetRegion}); orphaning ${orphanLogicalIds.length} resource(s): ${orphanLogicalIds.join(", ")}`);
74864
74916
  const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
74865
- if (!options.dryRun) await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "orphan");
74917
+ if (!options.dryRun) {
74918
+ if (!await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "orphan")) throw new Error(`Could not acquire lock for stack '${stackInfo.stackName}' (${targetRegion}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${stackInfo.stackName} --stack-region ${targetRegion}' if you are certain no other process is active.`);
74919
+ }
74866
74920
  try {
74867
74921
  const stateData = await stateBackend.getState(stackInfo.stackName, targetRegion);
74868
74922
  if (!stateData) throw new Error(`No state found for stack '${stackInfo.stackName}' (${targetRegion}). Nothing to orphan. (Did the stack get deployed?)`);
@@ -75447,7 +75501,7 @@ async function importCommand(stackArg, options) {
75447
75501
  }
75448
75502
  for (const overrideId of overrides.keys()) if (!templateLogicalIds.has(overrideId)) throw new Error(`--resource / --resource-mapping references logical ID '${overrideId}' which is not in the synthesized template for stack '${stackInfo.stackName}'. Available IDs: ${[...templateLogicalIds].join(", ")}`);
75449
75503
  const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
75450
- await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "import");
75504
+ if (!await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "import")) throw new Error(`Could not acquire lock for stack '${stackInfo.stackName}' (${targetRegion}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${stackInfo.stackName} --stack-region ${targetRegion}' if you are certain no other process is active.`);
75451
75505
  try {
75452
75506
  const rows = [];
75453
75507
  for (const { logicalId, resource } of resources) {
@@ -76176,7 +76230,7 @@ async function importNestedStackChildrenRecursive(args) {
76176
76230
  const childRewritten = rewriteTemplateAssetReferences(childTemplate, assetRedirect);
76177
76231
  if (childRewritten > 0) logger.info(`Note: ${childRewritten} asset reference(s) in nested stack ${childStackName} are recorded in state at their pre-rewrite (CDK bootstrap) values, so the next 'cdkd deploy' repoints any resource that still holds them to cdkd asset storage.`);
76178
76232
  }
76179
- await lockManager.acquireLock(childStackName, childRegion, lockOwner, "import");
76233
+ if (!await lockManager.acquireLock(childStackName, childRegion, lockOwner, "import")) throw new Error(`Could not acquire lock for nested stack '${childStackName}' (${childRegion}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${childStackName} --stack-region ${childRegion}' if you are certain no other process is active.`);
76180
76234
  try {
76181
76235
  const childResources = collectImportableResources(childTemplate);
76182
76236
  const childTemplateLogicalIds = new Set(childResources.map((r) => r.logicalId));
@@ -86091,7 +86145,7 @@ function createMigrateCommand() {
86091
86145
  */
86092
86146
  function buildProgram() {
86093
86147
  const program = new Command();
86094
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.36");
86148
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.38");
86095
86149
  program.hook("preAction", (_thisCommand, actionCommand) => {
86096
86150
  const { profile } = actionCommand.optsWithGlobals();
86097
86151
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;