@go-to-k/cdkd 0.283.31 → 0.283.32

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 coerceCfnBoolean, $t as getLegacyStateBucketName, A as cyan, An as DeployCancelledError, At as buildAssetRedirectMap, B as findSilentDropProperties, Bn as ResourceUpdateNotSupportedError, Bt as validateAssetBucketName, C as makeCanonicalizePropertiesFn, Cn as getAwsClients, Ct as LockManager, D as renderStatefulReason, Dn as CdkdError, Dt as AssetPublisher, E as isStatefulRecreateTargetSync, Et as shouldRetainResource, F as IAMRoleProvider, Fn as MissingCdkCliError, Ft as AssetModeResolver, G as IntrinsicFunctionResolver, Gt as getDockerCmd, H as slowCcOperationTimeoutMs, Hn as StackTerminationProtectionError, Ht as buildDenyExternalAccessPolicy, I as collectInlinePolicyNamesManagedBySiblings, In as NestedStackChildDirectDestroyError, It as BOOTSTRAP_MARKER_PREFIX, J as refStateLookupFromResource, Jn as withErrorHandling, Jt as AssetManifestLoader, K as cfnRefValueFromPhysicalId, Kt as runDockerForeground, L as clearOnUpdateRemoval, Ln as PartialFailureError, Lt as ensureAssetStorage, M as green, Mn as LocalMigrateError, Mt as loadPublishableAssetManifest, N as red, Nn as LocalStartServiceError, Nt as rewriteTemplateAssetReferences, O as formatResourceLine, Ot as stringifyValue, P as yellow, Pt as escapeRegExp, Q as assertRegionMatch, Qn as markNonRetryable, Qt as getDefaultStateBucketName, R as ProviderRegistry, Rn as ProvisioningError, Rt as getBootstrapMarkerKey, S as unsupportedFinalSnapshotError, Sn as AwsClients, St as TemplateParser, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as setAwsClients, Tt as rebuildClientForBucketRegion, U as disableInstanceApiTermination, Ut as buildDockerImage, V as CloudControlProvider, Vn as StackHasActiveImportsError, Vt as validateContainerRepoName, W as isTerminationProtectionPropagationError, Wt as formatDockerLoginError, X as normalizeAwsTagsToCfn, Xn as isRetryableTransientError, Xt as Synthesizer, Y as WAFv2WebACLProvider, Yn as isMarkedNonRetryable, Yt as getDockerImageBySourceHash, Z as resolveExplicitPhysicalId, Zn as isThrottlingError, Zt as synthesisStatusMessage, _ as buildFinalSnapshotIdentifier, _n as derivePartitionAndUrlSuffix, _t as DiffCalculator, a as DeploymentEventsStore, an as resolveStateBucketWithDefaultAndSource, at as requireConfigObject, b as isFinalSnapshotError, bt as withRetry, c as replayFailedOperations, cn as warnDeprecatedNoPrefixCliFlag, ct as TEMPLATE_SOURCED_RULES, d as deleteSkipReason, dn as MIGRATE_TMP_PREFIX, dt as s3BucketArn, en as resolveApp, et as configBooleanRefusal, f as withResourceDeadline, fn as findLargeInlineResources, ft as s3BucketDomainName, g as PRE_DELETE_SNAPSHOT_TYPES, gn as canonicalizeRegion, gt as applyRoleArnIfSet, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as PARTITION_TABLE, ht as s3BucketWebsiteUrl, i as DeploymentEventsReader, in as resolveStateBucketWithDefault, it as requireConfigArray, j as gray, jn as LocalInvokeBuildError$1, jt as createAssetRedirectResolver, k as bold, kt as WorkGraph, l as replayRollback, ln as CFN_TEMPLATE_BODY_LIMIT, lt as redactSecretsForState, m as computeImplicitDeleteEdges, mn as expectedOwnerParam, mt as s3BucketRegionalDomainName, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as resolveCaptureObservedState, nt as readConfigString, o as planFailedOps, on as resolveUseCdkBootstrapAssets, ot as requireConfigString, p as IMPLICIT_DELETE_DEPENDENCIES, pn as uploadCfnTemplate, pt as s3BucketDualStackDomainName, q as getAccountInfo, qn as normalizeAwsError, qt as runDockerStreaming, r as DeployEngine, rn as resolveSkipPrefix, rt as replayWarn, s as planRollback, sn as stateBucketExistenceConfirmed, st as STATE_SOURCED_CROSS_GENERATION_RULES, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveAutoAssetStorage, tt as configStringRefusal, u as UNSPECIFIED_SKIP_REASON, un as CFN_TEMPLATE_URL_LIMIT, ut as scrubResourceRecord, v as ccRoutedFinalSnapshotError, vn as AssemblyReader, vt as INTRINSIC_KEYS, w as extractDeploymentEventError, wn as resetAwsClients, wt as S3StateBackend, x as refusesFinalSnapshot, xn as resolveBucketRegion, xt as DagBuilder, y as createPreDeleteFinalSnapshot, yn as processStackMessages, yt as describeTypeWithThrottleRetry, z as findActionableSilentDrops, zn as ResourceTimeoutError, zt as parseBootstrapMarker } from "./deploy-engine-AWFiXyhp.js";
2
+ import { $ as getAccountInfo, $t as runDockerForeground, A as cyan, An as getAwsClients, At as S3StateBackend, B as stateKeySecretExposure, Bn as LocalStartServiceError, Bt as escapeRegExp, C as makeCanonicalizePropertiesFn, Cn as canonicalizeRegion, Ct as DiffCalculator, D as renderStatefulReason, Dt as DagBuilder, E as isStatefulRecreateTargetSync, En as processStackMessages, Et as withRetry, F as collectDeclaredOutputNames, Ft as WorkGraph, G as findActionableSilentDrops, Gn as ProvisioningError, Gt as getBootstrapMarkerKey, H as collectInlinePolicyNamesManagedBySiblings, Hn as MissingCdkCliError, Ht as AssetModeResolver, I as collectPublishedOutputNames, It as buildAssetRedirectMap, J as slowCcOperationTimeoutMs, Jn as StackHasActiveImportsError, Jt as validateContainerRepoName, K as findSilentDropProperties, Kn as ResourceTimeoutError, Kt as parseBootstrapMarker, L as exportAliasCollisionScrubWarning, Ln as DeployCancelledError, Lt as createAssetRedirectResolver, M as green, Mn as setAwsClients, Mt as shouldRetainResource, N as red, Nt as AssetPublisher, O as formatResourceLine, On as resolveBucketRegion, Ot as TemplateParser, P as yellow, Pn as CdkdError, Pt as stringifyValue, Q as cfnRefValueFromPhysicalId, Qt as getDockerCmd, R as isExportAliasCollision, Rn as LocalInvokeBuildError$1, Rt as loadPublishableAssetManifest, S as unsupportedFinalSnapshotError, Sn as PARTITION_TABLE, St as applyRoleArnIfSet, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as AssemblyReader, Tt as describeTypeWithThrottleRetry, U as clearOnUpdateRemoval, Un as NestedStackChildDirectDestroyError, Ut as BOOTSTRAP_MARKER_PREFIX, V as IAMRoleProvider, Vt as stripControlChars, W as ProviderRegistry, Wn as PartialFailureError, Wt as ensureAssetStorage, X as isTerminationProtectionPropagationError, Xt as buildDockerImage, Y as disableInstanceApiTermination, Yn as StackTerminationProtectionError, Yt as buildDenyExternalAccessPolicy, Z as IntrinsicFunctionResolver, Zt as formatDockerLoginError, _ as buildFinalSnapshotIdentifier, _n as CFN_TEMPLATE_URL_LIMIT, _t as s3BucketArn, a as DeploymentEventsStore, an as getDefaultStateBucketName, ar as markNonRetryable, at as coerceCfnBoolean, b as isFinalSnapshotError, bn as uploadCfnTemplate, bt as s3BucketRegionalDomainName, c as replayFailedOperations, cn as resolveAutoAssetStorage, ct as readConfigString, d as deleteSkipReason, dn as resolveStateBucketWithDefault, dt as requireConfigObject, en as runDockerStreaming, er as normalizeAwsError, et as refStateLookupFromResource, f as withResourceDeadline, fn as resolveStateBucketWithDefaultAndSource, ft as requireConfigString, g as PRE_DELETE_SNAPSHOT_TYPES, gn as CFN_TEMPLATE_BODY_LIMIT, gt as scrubResourceRecord, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as warnDeprecatedNoPrefixCliFlag, ht as redactSecretsForState, i as DeploymentEventsReader, in as synthesisStatusMessage, ir as isThrottlingError, it as assertRegionMatch, j as gray, jn as resetAwsClients, jt as rebuildClientForBucketRegion, k as bold, kn as AwsClients, kt as LockManager, l as replayRollback, ln as resolveCaptureObservedState, lt as replayWarn, m as computeImplicitDeleteEdges, mn as stateBucketExistenceConfirmed, mt as TEMPLATE_SOURCED_RULES, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as getDockerImageBySourceHash, nr as isMarkedNonRetryable, nt as normalizeAwsTagsToCfn, o as planFailedOps, on as getLegacyStateBucketName, ot as configBooleanRefusal, p as IMPLICIT_DELETE_DEPENDENCIES, pn as resolveUseCdkBootstrapAssets, pt as STATE_SOURCED_CROSS_GENERATION_RULES, q as CloudControlProvider, qn as ResourceUpdateNotSupportedError, qt as validateAssetBucketName, r as DeployEngine, rn as Synthesizer, rr as isRetryableTransientError, rt as resolveExplicitPhysicalId, s as planRollback, sn as resolveApp, st as configStringRefusal, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as AssetManifestLoader, tr as withErrorHandling, tt as WAFv2WebACLProvider, u as UNSPECIFIED_SKIP_REASON, un as resolveSkipPrefix, ut as requireConfigArray, v as ccRoutedFinalSnapshotError, vn as MIGRATE_TMP_PREFIX, vt as s3BucketDomainName, w as extractDeploymentEventError, wn as derivePartitionAndUrlSuffix, wt as INTRINSIC_KEYS, x as refusesFinalSnapshot, xn as expectedOwnerParam, xt as s3BucketWebsiteUrl, y as createPreDeleteFinalSnapshot, yn as findLargeInlineResources, yt as s3BucketDualStackDomainName, z as secretBearingStateKeyWarning, zn as LocalMigrateError, zt as rewriteTemplateAssetReferences } from "./deploy-engine-rYTNzst0.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
4
- import { t as ASGProvider } from "./asg-provider-SQ0V0nFd.js";
4
+ import { t as ASGProvider } from "./asg-provider-CVoisQ0L.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";
@@ -57389,6 +57389,25 @@ function isSecretDynamicReference(value) {
57389
57389
  if (typeof value !== "string") return false;
57390
57390
  return value.includes("{{resolve:secretsmanager:") || value.includes("{{resolve:ssm-secure:");
57391
57391
  }
57392
+ /**
57393
+ * The same question asked of a whole template VALUE, walking every string leaf.
57394
+ *
57395
+ * The leaf predicate answers `false` for a non-string, and an output's `Value`
57396
+ * is very often an OBJECT: `secret.secretValueFromJson(...)` renders the
57397
+ * secret's ARN as a `Ref`, so the value is an `Fn::Join` / `Fn::Sub` — which
57398
+ * CLAUDE.md's #1916 note calls the DOMINANT CDK shape, not an edge case.
57399
+ * Feeding the raw value to the leaf predicate therefore reported "no secret
57400
+ * here" for exactly the templates most likely to have one, which silently
57401
+ * disarmed BOTH signals built on it: the export-alias decision below (issue
57402
+ * #1919) and the legacy-record withholding that keeps a pre-GHSA plaintext out
57403
+ * of the rendered diff (issue #1921).
57404
+ */
57405
+ function containsSecretDynamicReference(value) {
57406
+ if (typeof value === "string") return isSecretDynamicReference(value);
57407
+ if (Array.isArray(value)) return value.some(containsSecretDynamicReference);
57408
+ if (value !== null && typeof value === "object") return Object.values(value).some(containsSecretDynamicReference);
57409
+ return false;
57410
+ }
57392
57411
  /** Structural equality, mirroring the deploy engine's `outputMapsEqual` leaf rule. */
57393
57412
  function valuesEqual(a, b) {
57394
57413
  if (a === b) return true;
@@ -57446,6 +57465,7 @@ async function resolveTemplateOutputs(template, resolveFn, conditions) {
57446
57465
  const failedKeys = /* @__PURE__ */ new Set();
57447
57466
  const secretSourceKeys = /* @__PURE__ */ new Set();
57448
57467
  let resolutionFailed = false;
57468
+ const publishedOutputNames = collectPublishedOutputNames(template.Outputs ?? {}, conditions);
57449
57469
  if (!template.Outputs) return {
57450
57470
  outputs,
57451
57471
  exportNames,
@@ -57464,7 +57484,7 @@ async function resolveTemplateOutputs(template, resolveFn, conditions) {
57464
57484
  if (typeof output.Export?.Name === "string") failedKeys.add(output.Export.Name);
57465
57485
  };
57466
57486
  for (const [outputKey, output] of Object.entries(template.Outputs)) {
57467
- if (!isSecretDynamicReference(output.Value)) continue;
57487
+ if (!containsSecretDynamicReference(output.Value)) continue;
57468
57488
  secretSourceKeys.add(outputKey);
57469
57489
  if (typeof output.Export?.Name === "string") secretSourceKeys.add(output.Export.Name);
57470
57490
  }
@@ -57493,6 +57513,7 @@ async function resolveTemplateOutputs(template, resolveFn, conditions) {
57493
57513
  if (output.Export?.Name) {
57494
57514
  let exportName = output.Export.Name;
57495
57515
  const exportSourceUsedSub = templateUsesSub(output.Export.Name);
57516
+ const declaredExportIsIntrinsic = typeof output.Export.Name !== "string";
57496
57517
  if (typeof exportName !== "string") try {
57497
57518
  exportName = await resolveFn(structuredClone(exportName));
57498
57519
  } catch (error) {
@@ -57500,10 +57521,17 @@ async function resolveTemplateOutputs(template, resolveFn, conditions) {
57500
57521
  resolutionFailed = true;
57501
57522
  continue;
57502
57523
  }
57503
- if (typeof exportName === "string" && !isUnresolvedValue(exportName, exportSourceUsedSub)) {
57524
+ if (typeof exportName === "string" && !isUnresolvedValue(exportName, exportSourceUsedSub)) if (declaredExportIsIntrinsic && isSecretDynamicReference(exportName)) logger.debug(`Diff skipping export alias of ${stripControlChars(outputKey)} — the name carries a secret reference`);
57525
+ else if (isExportAliasCollision(exportName, outputKey, publishedOutputNames)) logger.debug(`Diff skipping export alias ${stripControlChars(exportName)} of ${stripControlChars(outputKey)} — collides with an output name`);
57526
+ else if (!declaredExportIsIntrinsic && secretSourceKeys.size > 0) {
57527
+ logger.debug(`Diff cannot decide the export alias of ${stripControlChars(outputKey)} — a literal name may contain a resolved secret`);
57528
+ failedKeys.add(exportName);
57529
+ resolutionFailed = true;
57530
+ } else {
57504
57531
  outputs[exportName] = value;
57505
57532
  exportNames.add(exportName);
57506
- } else resolutionFailed = true;
57533
+ }
57534
+ else resolutionFailed = true;
57507
57535
  }
57508
57536
  }
57509
57537
  return {
@@ -57553,8 +57581,8 @@ function computeOutputsDiff(current, desired, exportNames, secretSourceKeys = /*
57553
57581
  const changes = [];
57554
57582
  const currentBag = current ?? {};
57555
57583
  const legacyRecord = Object.entries(currentBag).some(([name, oldValue]) => {
57556
- if (isSecretDynamicReference(oldValue)) return false;
57557
- return isSecretDynamicReference(desired[name]) || secretSourceKeys.has(name);
57584
+ if (typeof oldValue === "string" && isSecretDynamicReference(oldValue)) return false;
57585
+ return containsSecretDynamicReference(desired[name]) || secretSourceKeys.has(name);
57558
57586
  });
57559
57587
  const push = (change) => {
57560
57588
  if (legacyRecord && change.changeType !== "ADD") {
@@ -58988,10 +59016,10 @@ const REDACTED_LEGACY_PLAINTEXT = "<redacted: legacy plaintext in state — run
58988
59016
  * consumer may match on, and silently mutating it there would trade a real
58989
59017
  * correctness regression for a display concern belonging to whatever renders
58990
59018
  * the JSON. `JSON.stringify` already escapes everything below 0x20.
59019
+ *
59020
+ * The helper itself now lives in `src/utils/regexp.ts` — `outputs-export-alias`
59021
+ * prints the same class of string and had grown a second copy.
58991
59022
  */
58992
- function stripControlChars(value) {
58993
- return value.replace(/[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
58994
- }
58995
59023
  /**
58996
59024
  * The same guard for an already-JSON-SERIALIZED value: C1 and the bidi marks
58997
59025
  * only, deliberately NOT C0.
@@ -59211,9 +59239,11 @@ function createDiffCommand() {
59211
59239
  //#endregion
59212
59240
  //#region src/cli/commands/scrub.ts
59213
59241
  /**
59214
- * Signals `cdkd scrub` found stacks whose state still holds plaintext secrets.
59215
- * Only thrown under `--dry-run --fail`; carries no message (the plan was
59216
- * already printed) and maps to a non-zero exit so CI can gate on it.
59242
+ * Signals `cdkd scrub` found plaintext it is reporting rather than removing.
59243
+ * Thrown under `--fail`: with `--dry-run` when any plaintext secret is in state,
59244
+ * and on a REAL run when a leak was found that scrub cannot rewrite (a
59245
+ * secret-bearing output KEY, issue #1919). Carries no message — the plan was
59246
+ * already printed — and maps to a non-zero exit so CI can gate on it.
59217
59247
  */
59218
59248
  var ScrubNeededError = class ScrubNeededError extends CdkdError {
59219
59249
  silent = true;
@@ -59312,6 +59342,7 @@ async function scrubCommand(stacks, options) {
59312
59342
  });
59313
59343
  const lockManager = new LockManager(stateS3.s3, stateConfig);
59314
59344
  let totalStacksScrubbed = 0;
59345
+ let totalStacksWithUnscrubbableKeys = 0;
59315
59346
  for (const stack of targetStacks) {
59316
59347
  const scrubbed = await scrubStack(stack, stack.region || region, stateBackend, lockManager, {
59317
59348
  dryRun: options.dryRun ?? false,
@@ -59321,18 +59352,26 @@ async function scrubCommand(stacks, options) {
59321
59352
  if (scrubbed.recordsChanged > 0) {
59322
59353
  totalStacksScrubbed++;
59323
59354
  logger.info(`${options.dryRun ? "Would scrub" : "Scrubbed"} ${scrubbed.recordsChanged} resource record(s) in ${stack.stackName}`);
59324
- } else logger.info(`No plaintext secrets found in ${stack.stackName}`);
59355
+ } else if (scrubbed.secretBearingKeys === 0) logger.info(`No plaintext secrets found in ${stack.stackName}`);
59356
+ if (scrubbed.secretBearingKeys > 0) {
59357
+ totalStacksWithUnscrubbableKeys++;
59358
+ logger.warn(`${scrubbed.secretBearingKeys} output KEY(s) in ${stack.stackName} hold plaintext and CANNOT be scrubbed — rename the Export.Name and redeploy (see the warning above).`);
59359
+ }
59325
59360
  }
59326
- if (totalStacksScrubbed === 0) {
59361
+ if (totalStacksScrubbed === 0 && totalStacksWithUnscrubbableKeys === 0) {
59327
59362
  logger.info("\nNo plaintext secrets found in any target stack state. Nothing to scrub.");
59328
59363
  return;
59329
59364
  }
59365
+ const keyNote = totalStacksWithUnscrubbableKeys > 0 ? ` ${totalStacksWithUnscrubbableKeys} stack(s) hold plaintext in an output KEY, which cdkd scrub cannot rewrite — rename that output's Export.Name and redeploy.` : "";
59330
59366
  if (options.dryRun) {
59331
- logger.info(`\nPlan: ${totalStacksScrubbed} stack(s) hold plaintext secrets and would be scrubbed (--dry-run, no state written). ROTATE any exposed secret in Secrets Manager.`);
59367
+ if (totalStacksScrubbed > 0) logger.info(`\nPlan: ${totalStacksScrubbed} stack(s) hold plaintext secrets and would be scrubbed (--dry-run, no state written).${keyNote} ROTATE any exposed secret in Secrets Manager.`);
59368
+ else logger.info(`\nPlan: nothing can be scrubbed.${keyNote} ROTATE any exposed secret.`);
59332
59369
  if (options.fail) throw new ScrubNeededError();
59333
59370
  return;
59334
59371
  }
59335
- logger.info(`\nDone: scrubbed ${totalStacksScrubbed} stack(s). The plaintext is no longer stored, but a value that was ever persisted should be treated as compromised — ROTATE it in Secrets Manager (scrub matches the current value, so scrub BEFORE rotating).`);
59372
+ if (totalStacksScrubbed > 0) logger.info(`\nDone: scrubbed ${totalStacksScrubbed} stack(s). The plaintext is no longer stored there, but a value that was ever persisted should be treated as compromised — ROTATE it in Secrets Manager (scrub matches the current value, so scrub BEFORE rotating).${keyNote}`);
59373
+ else logger.info(`\nNothing could be rewritten.${keyNote} ROTATE any exposed secret.`);
59374
+ if (options.fail && totalStacksWithUnscrubbableKeys > 0) throw new ScrubNeededError();
59336
59375
  }
59337
59376
  /**
59338
59377
  * Scrub one stack's state. Re-resolves the template's per-resource properties to
@@ -59351,7 +59390,8 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
59351
59390
  logger.debug(`No state for ${stack.stackName} (${region}) — skipping`);
59352
59391
  return {
59353
59392
  recordsChanged: 0,
59354
- secretsFound: 0
59393
+ secretsFound: 0,
59394
+ secretBearingKeys: 0
59355
59395
  };
59356
59396
  }
59357
59397
  const state = loaded.state;
@@ -59399,12 +59439,42 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
59399
59439
  perResourceTemplateProps.set(logicalId, templateResource.Properties);
59400
59440
  }
59401
59441
  const templateOutputs = stack.template.Outputs ?? {};
59442
+ const declaredOutputNames = collectDeclaredOutputNames(templateOutputs);
59443
+ const ambiguousKeys = /* @__PURE__ */ new Set();
59444
+ const collisions = [];
59445
+ let outputsSourceUntrusted = false;
59446
+ for (const [name, output] of Object.entries(templateOutputs)) {
59447
+ const declaredExportName = output.Export?.Name;
59448
+ let exportName = declaredExportName;
59449
+ if (declaredExportName !== void 0 && typeof declaredExportName !== "string") try {
59450
+ exportName = await resolver.resolve(declaredExportName, {
59451
+ template: stack.template,
59452
+ resources: state.resources,
59453
+ ...Object.keys(parameters).length > 0 && { parameters },
59454
+ ...Object.keys(conditions).length > 0 && { conditions },
59455
+ stackName: stack.stackName,
59456
+ recordedSecretValues: outputSecrets,
59457
+ bestEffort: true
59458
+ });
59459
+ } catch (err) {
59460
+ outputsSourceUntrusted = true;
59461
+ logger.warn(`Export.Name of output ${name} could not be resolved during scrub (${err instanceof Error ? err.message : String(err)}) — redacting this stack's outputs by value match instead of by template position, since state may be keyed under a name this run cannot reproduce.`);
59462
+ }
59463
+ if (declaredExportName !== void 0 && typeof exportName !== "string") outputsSourceUntrusted = true;
59464
+ if (declaredExportName !== void 0 && typeof exportName === "string" && isUnresolvedValue(exportName, templateUsesSub(declaredExportName))) {
59465
+ outputsSourceUntrusted = true;
59466
+ logger.warn(`Export.Name of output ${name} did not fully resolve during scrub — redacting this stack's outputs by value match instead of by template position, since state may be keyed under a name this run cannot reproduce.`);
59467
+ } else if (typeof exportName === "string" && isExportAliasCollision(exportName, name, declaredOutputNames)) {
59468
+ ambiguousKeys.add(exportName);
59469
+ collisions.push([name, exportName]);
59470
+ }
59471
+ }
59402
59472
  for (const [name, output] of Object.entries(templateOutputs)) {
59403
59473
  const value = output.Value;
59404
59474
  if (value === void 0) continue;
59405
- outputsTemplateSource[name] = value;
59475
+ if (!ambiguousKeys.has(name)) outputsTemplateSource[name] = value;
59406
59476
  const exportName = output.Export?.Name;
59407
- if (typeof exportName === "string") outputsTemplateSource[exportName] = value;
59477
+ if (typeof exportName === "string" && !ambiguousKeys.has(exportName)) outputsTemplateSource[exportName] = value;
59408
59478
  try {
59409
59479
  await resolver.resolve(value, {
59410
59480
  template: stack.template,
@@ -59419,10 +59489,19 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
59419
59489
  logger.debug(`Resolution of output ${name} during scrub was partial: ${err instanceof Error ? err.message : String(err)}`);
59420
59490
  }
59421
59491
  }
59492
+ for (const [name, exportName] of collisions) logger.warn(exportAliasCollisionScrubWarning(name, exportName, outputSecrets));
59493
+ const secretBearingKeys = [];
59494
+ for (const key of Object.keys(state.outputs ?? {})) {
59495
+ const exposure = stateKeySecretExposure(key, outputSecrets);
59496
+ if (!exposure) continue;
59497
+ secretBearingKeys.push(key);
59498
+ logger.warn(secretBearingStateKeyWarning(stack.stackName, key, exposure));
59499
+ }
59422
59500
  const totalSecrets = outputSecrets.size + [...perResourceSecrets.values()].reduce((n, m) => n + m.size, 0);
59423
59501
  if (totalSecrets === 0) return {
59424
59502
  recordsChanged: 0,
59425
- secretsFound: 0
59503
+ secretsFound: 0,
59504
+ secretBearingKeys: 0
59426
59505
  };
59427
59506
  let recordsChanged = 0;
59428
59507
  const newResources = {};
@@ -59438,7 +59517,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
59438
59517
  if (JSON.stringify(scrubbed) !== JSON.stringify(record)) recordsChanged++;
59439
59518
  newResources[logicalId] = scrubbed;
59440
59519
  }
59441
- const newOutputs = outputSecrets.size > 0 ? redactSecretsForState(state.outputs, outputSecrets, outputsTemplateSource) : state.outputs;
59520
+ const newOutputs = outputSecrets.size > 0 ? redactSecretsForState(state.outputs, outputSecrets, outputsSourceUntrusted ? void 0 : outputsTemplateSource) : state.outputs;
59442
59521
  if (JSON.stringify(newOutputs) !== JSON.stringify(state.outputs)) recordsChanged++;
59443
59522
  if (recordsChanged > 0 && !opts.dryRun) {
59444
59523
  const nextState = {
@@ -59451,7 +59530,8 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
59451
59530
  }
59452
59531
  return {
59453
59532
  recordsChanged,
59454
- secretsFound: totalSecrets
59533
+ secretsFound: totalSecrets,
59534
+ secretBearingKeys: secretBearingKeys.length
59455
59535
  };
59456
59536
  } finally {
59457
59537
  if (acquired) await lockManager.releaseLock(stack.stackName, region).catch((err) => {
@@ -59460,7 +59540,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
59460
59540
  }
59461
59541
  }
59462
59542
  function createScrubCommand() {
59463
- const cmd = new Command("scrub").description("Rewrite persisted state so resolved secret dynamic references are stored as their {{resolve:...}} expression, not the plaintext value (no deploy).").argument("[stacks...]", "Stack name(s) to scrub (physical name or display path)").option("--all", "Scrub every stack in the synthesized app", false).option("--dry-run", "Report what would be scrubbed without writing state").option("--fail", "With --dry-run, exit non-zero if any plaintext secret is found (CI gate)");
59543
+ const cmd = new Command("scrub").description("Rewrite persisted state so resolved secret dynamic references are stored as their {{resolve:...}} expression, not the plaintext value (no deploy).").argument("[stacks...]", "Stack name(s) to scrub (physical name or display path)").option("--all", "Scrub every stack in the synthesized app", false).option("--dry-run", "Report what would be scrubbed without writing state").option("--fail", "With --dry-run, exit non-zero if any plaintext secret is found (CI gate). Also exits non-zero on a real run when a leak was found that scrub cannot rewrite.");
59464
59544
  [
59465
59545
  ...commonOptions,
59466
59546
  ...appOptions,
@@ -80334,7 +80414,7 @@ function createMigrateCommand() {
80334
80414
  */
80335
80415
  function buildProgram() {
80336
80416
  const program = new Command();
80337
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.31");
80417
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.32");
80338
80418
  program.hook("preAction", (_thisCommand, actionCommand) => {
80339
80419
  const { profile } = actionCommand.optsWithGlobals();
80340
80420
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;