@go-to-k/cdkd 0.290.2 → 0.290.4

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 getCurrentResourceSecrets, $n as validateAssetBucketName, $t as findActionableSilentDrops, A as planOrphanAdoption, An as CUSTOM_RESOURCE_RESPONSE_PREFIX, Ar as warnDeprecatedNoPrefixCliFlag, At as resolveExplicitPhysicalId, B as bold, Bn as WorkGraph, Br as derivePartitionAndUrlSuffix, Bt as requireConfigObject, C as createPreDeleteFinalSnapshot, Ci as markNonRetryable, Cn as S3StateBackend, Cr as resolveAutoAssetStorage, Ct as coerceParameterTypedValue, D as makeCanonicalizePropertiesFn, Dn as forceQuitRecoveryClause, Dr as resolveStateBucketWithDefaultAndSource, Dt as refStateLookupFromResource, E as unsupportedFinalSnapshotError, En as buildLockContentionMessage, Er as resolveStateBucketWithDefault, Et as parameterTypeMayLoseSecretIdentity, F as WARM_THROUGHPUT_MEMBERS, Fn as orphansAfterRollback, Fr as uploadCfnTemplate, Ft as configIntegerRefusal, G as yellow, Gn as AssetModeResolver, Gr as AwsClients, Gt as s3BucketArn, H as gray, Hn as createAssetRedirectResolver, Hr as processStackMessages, Ht as classifyReplaySecretRegion, I as coerceWarmThroughput$1, In as orphansCarriedFrom, Ir as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, It as configStringRefusal, J as exportAliasCollisionScrubWarning, Jn as ensureAssetStorage, Jr as setAwsClients, Jt as s3BucketRegionalDomainName, K as collectDeclaredOutputNames, Kn as BOOTSTRAP_MARKER_PREFIX, Kr as getAwsClients, Kt as s3BucketDomainName, L as isWarmThroughputDecrease, Ln as shouldRetainResource, Lr as expectedOwnerParam, Lt as readConfigString, M as isStatefulRecreateTargetForReplace, Mn as exportNamesCarriedFrom, Mr as CFN_TEMPLATE_URL_LIMIT, Mt as coerceCfnBoolean, N as isStatefulRecreateTargetSync, Nn as importableOutputKeys, Nr as MIGRATE_TMP_PREFIX, Nt as coerceCfnInteger, O as extractDeploymentEventError, On as formatLockExpiry, Or as resolveUseCdkBootstrapAssets, Ot as WAFv2WebACLProvider, P as renderStatefulReason, Pn as importableOutputs, Pr as findLargeInlineResources, Pt as configBooleanRefusal, Q as secretSafeKeyDisplay, Qn as readBootstrapMarkerBody, Qr as CrossAccountSecretRefusalError, Qt as INTRINSIC_KEYS, R as toFiniteNumber, Rn as AssetPublisher, Rr as PARTITION_TABLE, Rt as replayWarn, S as ccRoutedFinalSnapshotError, Si as isTransientServerError, Sn as LockManager, Sr as resolveApp, St as cfnRefValueFromPhysicalId, T as refusesFinalSnapshot, Ti as retryClassificationText, Tn as buildForceUnlockCommand, Tr as resolveSkipPrefix, Tt as isUnboundTemplateParameter, U as green, Un as loadPublishableAssetManifest, Ut as producerRegionsFromState, V as cyan, Vn as buildAssetRedirectMap, Vr as AssemblyReader, Vt as requireConfigString, W as red, Wn as rewriteTemplateAssetReferences, Wr as resolveBucketRegion, Wt as withSharedDrainBudget, X as secretBearing, Xn as isCrossRegionRedirect, Xr as CdkdError, Xt as applyRoleArnIfSet, Y as isExportAliasCollision, Yn as getBootstrapMarkerKey, Yt as s3BucketWebsiteUrl, Z as secretBearingStateKeyWarning, Zn as parseBootstrapMarker, Zt as DiffCalculator, _ as isSecretBearingReferenceString, _i as normalizeAwsError, _n as maskSecretsInText, _r as getDockerImageBySourceHash, _t as deleteSkipReason, a as DeploymentEventsStore, an as STATE_SOURCED_BASELINE_RULES, ar as describeDockerExecFailure, at as createMaskedRetryLogger, b as PRE_DELETE_SNAPSHOT_TYPES, bi as isRetryableTransientError, bn as redactSecretsForState, br as getDefaultStateBucketName, bt as IntrinsicFunctionResolver, c as replayFailedOperations, ci as ProvisioningError, cn as TEMPLATE_SOURCED_RULES, cr as formatDockerLoginError, ct as beginCommandInterruptScope, d as updatePartialReason, di as StackHasActiveImportsError, dn as dynamicReferenceTokens, dr as redactDockerArgvValues, dt as isInterruptedWaitError, ei as DeployCancelledError, en as findSilentDropProperties, er as validateContainerRepoName, et as IAMRoleProvider, f as withResourceDeadline, fi as StackTerminationProtectionError, fn as errorCauseChain, fr as runDockerForeground, ft as startInterruptWatch, g as bagHoldsSecretExpression, gn as maskSecretsInError, gr as AssetManifestLoader, gt as deleteIndeterminateGuards, h as computeImplicitDeleteEdges, hn as markSameGenerationBag, hr as stripControlChars, ht as UNSPECIFIED_SKIP_REASON, i as DeploymentEventsReader, ii as LocalStartServiceError, in as TemplateParser, ir as describeDockerCapturedOutput, it as wouldReturnToSdkProvider, j as MULTI_REGION_RECREATE_BLOCKED_TYPES, jn as DEFAULT_STATE_PREFIX, jr as CFN_TEMPLATE_BODY_LIMIT, jt as assertRegionMatch, k as makeSiblingClaimReader, kn as shellQuote, kr as stateBucketExistenceConfirmed, kt as normalizeAwsTagsToCfn, l as replayRollback, li as ResourceTimeoutError, ln as carriesSecretMask, lr as getDockerCmd, lt as endCommandInterruptScope, m as IMPLICIT_DELETE_DEPENDENCIES, mn as isSingleDynamicReferenceToken, mr as escapeRegExp, mt as slowCcOperationTimeoutMs, n as DEFAULT_RESOURCE_WARN_AFTER_MS, ni as IntrinsicResolutionRefusalError, nn as withRetry, nr as describeAwsFailure, nt as clearOnUpdateRemoval, o as planFailedOps, oi as NestedStackChildDirectDestroyError, on as STATE_SOURCED_CROSS_GENERATION_RULES, or as describeDockerFailure, ot as maskDeep, p as maskingRetryLogger, pn as identityKeyFor, pr as runDockerStreaming, pt as CloudControlProvider, q as collectPublishedOutputNames, qn as assertAssetBucketRegion, qr as resetAwsClients, qt as s3BucketDualStackDomainName, r as DeployEngine, ri as LocalInvokeBuildError$1, rn as DagBuilder, rr as buildDockerImage, rt as ProviderRegistry, s as planRollback, si as PartialFailureError, sn as STATE_SOURCED_READBACK_RULES, sr as dockerSpawnEnvWithSensitive, st as maskerOrIdentity, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as DynamicReferenceRegionAmbiguousError, tn as describeTypeWithThrottleRetry, tr as buildDenyExternalAccessPolicy, tt as collectInlinePolicyNamesManagedBySiblings, u as updatePartialMessage, ui as ResourceUpdateNotSupportedError, un as createSecretMasker, ur as partitionSensitiveEnv, ut as interruptWatchListenerCount, v as bindingSkippedOutputs, vi as withErrorHandling, vn as recordMaskOnlyValue, vr as Synthesizer, vt as disableInstanceApiTermination, w as isFinalSnapshotError, wi as markRedactedCause, wn as rebuildClientForBucketRegion, wr as resolveCaptureObservedState, wt as getAccountInfo, x as buildFinalSnapshotIdentifier, xi as isThrottlingError, xn as scrubResourceRecord, xr as getLegacyStateBucketName, xt as carriesDynamicReference, y as ATOMIC_FINAL_SNAPSHOT_TYPES, yi as isMarkedNonRetryable, yn as recoverMaskedOutput, yr as synthesisStatusMessage, yt as isTerminationProtectionPropagationError, z as formatResourceLine, zn as stringifyValue, zr as canonicalizeRegion, zt as requireConfigArray } from "./deploy-engine-Nn9j2GPK.js";
2
- import { _ as withSkipPrefix, b as displaySafe, 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 truncateCodePoints, y as UNRENDERABLE } from "./logger-DTEUzYP_.js";
1
+ import { $ as secretSafeKeyDisplay, $n as parseBootstrapMarker, $t as DiffCalculator, A as planOrphanAdoption, An as formatLockExpiry, Ar as resolveUseCdkBootstrapAssets, At as WAFv2WebACLProvider, B as formatResourceLine, Bn as AssetPublisher, Br as PARTITION_TABLE, Bt as replayWarn, C as createPreDeleteFinalSnapshot, Ci as isThrottlingError, Cn as scrubResourceRecord, Cr as getLegacyStateBucketName, Ct as cfnRefValueFromPhysicalId, D as makeCanonicalizePropertiesFn, Di as retryClassificationText, Dn as buildForceUnlockCommand, Dr as resolveSkipPrefix, Dt as parameterTypeMayLoseSecretIdentity, E as unsupportedFinalSnapshotError, Ei as markRedactedCause, En as rebuildClientForBucketRegion, Er as resolveCaptureObservedState, Et as isUnboundTemplateParameter, F as WARM_THROUGHPUT_MEMBERS, Fn as importableOutputKeys, Fr as MIGRATE_TMP_PREFIX, Ft as coerceCfnInteger, G as red, Gn as loadPublishableAssetManifest, Gt as producerRegionsFromState, H as cyan, Hn as WorkGraph, Hr as derivePartitionAndUrlSuffix, Ht as requireConfigObject, I as coerceWarmThroughput$1, In as importableOutputs, Ir as findLargeInlineResources, It as configBooleanRefusal, J as collectPublishedOutputNames, Jn as BOOTSTRAP_MARKER_PREFIX, Jr as getAwsClients, Jt as s3BucketDomainName, K as yellow, Kn as rewriteTemplateAssetReferences, Kr as resolveBucketRegion, Kt as withSharedDrainBudget, L as isWarmThroughputDecrease, Ln as orphansAfterRollback, Lr as uploadCfnTemplate, Lt as configIntegerRefusal, M as isStatefulRecreateTargetForReplace, Mn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Mr as warnDeprecatedNoPrefixCliFlag, Mt as resolveExplicitPhysicalId, N as isStatefulRecreateTargetSync, Nn as DEFAULT_STATE_PREFIX, Nr as CFN_TEMPLATE_BODY_LIMIT, Nt as assertRegionMatch, O as extractDeploymentEventError, On as buildLockContentionMessage, Or as resolveStateBucketWithDefault, Ot as refStateLookupFromResource, P as renderStatefulReason, Pn as exportNamesCarriedFrom, Pr as CFN_TEMPLATE_URL_LIMIT, Pt as coerceCfnBoolean, Q as secretBearingStateKeyWarning, Qn as isCrossRegionRedirect, Qr as CdkdError, Qt as applyRoleArnIfSet, R as toCfnInteger, Rn as orphansCarriedFrom, Rr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, Rt as configStringRefusal, S as ccRoutedFinalSnapshotError, Si as isRetryableTransientError, Sn as redactSecretsForState, Sr as getDefaultStateBucketName, St as carriesDynamicReference, T as refusesFinalSnapshot, Ti as markNonRetryable, Tn as S3StateBackend, Tr as resolveAutoAssetStorage, Tt as getAccountInfo, U as gray, Un as buildAssetRedirectMap, Ur as AssemblyReader, Ut as requireConfigString, V as bold, Vn as stringifyValue, Vr as canonicalizeRegion, Vt as requireConfigArray, W as green, Wn as createAssetRedirectResolver, Wr as processStackMessages, Wt as classifyReplaySecretRegion, X as isExportAliasCollision, Xn as ensureAssetStorage, Xr as setAwsClients, Xt as s3BucketRegionalDomainName, Y as exportAliasCollisionScrubWarning, Yn as assertAssetBucketRegion, Yr as resetAwsClients, Yt as s3BucketDualStackDomainName, Z as secretBearing, Zn as getBootstrapMarkerKey, Zt as s3BucketWebsiteUrl, _ as isSecretBearingReferenceString, _n as markSameGenerationBag, _r as stripControlChars, _t as deleteIndeterminateGuards, a as DeploymentEventsStore, ai as LocalInvokeBuildError$1, an as DagBuilder, ar as buildDockerImage, at as wouldReturnToSdkProvider, b as PRE_DELETE_SNAPSHOT_TYPES, bi as withErrorHandling, bn as recordMaskOnlyValue, br as Synthesizer, bt as isTerminationProtectionPropagationError, c as replayFailedOperations, ci as NestedStackChildDirectDestroyError, cn as STATE_SOURCED_CROSS_GENERATION_RULES, cr as describeDockerFailure, ct as maskerOrIdentity, d as updatePartialReason, di as ResourceTimeoutError, dn as carriesSecretMask, dr as getDockerCmd, dt as interruptWatchListenerCount, ei as CrossAccountSecretRefusalError, en as INTRINSIC_KEYS, er as readBootstrapMarkerBody, et as getCurrentResourceSecrets, f as withResourceDeadline, fi as ResourceUpdateNotSupportedError, fn as createSecretMasker, fr as partitionSensitiveEnv, ft as isInterruptedWaitError, g as bagHoldsSecretExpression, gn as isSingleDynamicReferenceToken, gr as escapeRegExp, gt as UNSPECIFIED_SKIP_REASON, h as computeImplicitDeleteEdges, hn as identityKeyFor, hr as runDockerStreaming, ht as slowCcOperationTimeoutMs, i as DeploymentEventsReader, ii as IntrinsicResolutionRefusalError, in as withRetry, ir as describeAwsFailure, it as ProviderRegistry, j as MULTI_REGION_RECREATE_BLOCKED_TYPES, jn as shellQuote, jr as stateBucketExistenceConfirmed, jt as normalizeAwsTagsToCfn, k as makeSiblingClaimReader, kn as forceQuitRecoveryClause, kr as resolveStateBucketWithDefaultAndSource, kt as isSettledInstanceState, l as replayRollback, li as PartialFailureError, ln as STATE_SOURCED_READBACK_RULES, lr as dockerSpawnEnvWithSensitive, lt as beginCommandInterruptScope, m as IMPLICIT_DELETE_DEPENDENCIES, mi as StackTerminationProtectionError, mn as errorCauseChain, mr as runDockerForeground, mt as CloudControlProvider, n as DEFAULT_RESOURCE_WARN_AFTER_MS, ni as DeployCancelledError, nn as findSilentDropProperties, nr as validateContainerRepoName, nt as collectInlinePolicyNamesManagedBySiblings, o as planFailedOps, oi as LocalStartServiceError, on as TemplateParser, or as describeDockerCapturedOutput, ot as createMaskedRetryLogger, p as maskingRetryLogger, pi as StackHasActiveImportsError, pn as dynamicReferenceTokens, pr as redactDockerArgvValues, pt as startInterruptWatch, q as collectDeclaredOutputNames, qn as AssetModeResolver, qr as AwsClients, qt as s3BucketArn, r as DeployEngine, ri as DynamicReferenceRegionAmbiguousError, rn as describeTypeWithThrottleRetry, rr as buildDenyExternalAccessPolicy, rt as clearOnUpdateRemoval, s as planRollback, sn as STATE_SOURCED_BASELINE_RULES, sr as describeDockerExecFailure, st as maskDeep, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as findActionableSilentDrops, tr as validateAssetBucketName, tt as IAMRoleProvider, u as updatePartialMessage, ui as ProvisioningError, un as TEMPLATE_SOURCED_RULES, ur as formatDockerLoginError, ut as endCommandInterruptScope, v as bindingSkippedOutputs, vn as maskSecretsInError, vr as AssetManifestLoader, vt as deleteSkipReason, w as isFinalSnapshotError, wi as isTransientServerError, wn as LockManager, wr as resolveApp, wt as coerceParameterTypedValue, x as buildFinalSnapshotIdentifier, xi as isMarkedNonRetryable, xn as recoverMaskedOutput, xr as synthesisStatusMessage, xt as IntrinsicFunctionResolver, y as ATOMIC_FINAL_SNAPSHOT_TYPES, yi as normalizeAwsError, yn as maskSecretsInText, yr as getDockerImageBySourceHash, yt as disableInstanceApiTermination, z as toFiniteNumber, zn as shouldRetainResource, zr as expectedOwnerParam, zt as readConfigString } from "./deploy-engine-DD7JE_Ic.js";
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-7XwrjRzX.js";
5
- import { r as protectedReplacementAdvice, t as ASGProvider } from "./asg-provider-BsC_XndT.js";
4
+ import { t as getCdkdVersion } from "./version-BUcQz4SU.js";
5
+ import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-NffO_bpL.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";
@@ -246,7 +246,7 @@ function parseContextOptions(contextArgs) {
246
246
  const commonOptions = [
247
247
  new Option("--verbose", "Enable verbose logging").default(false),
248
248
  new Option("--profile <profile>", "AWS profile"),
249
- new Option("--role-arn <arn>", "IAM role ARN to assume for AWS API calls (env: CDKD_ROLE_ARN); the role MUST have admin-equivalent permissions because cdkd issues raw service API calls and does not route through CloudFormation, so CDK CLI deploy-roles will NOT work"),
249
+ new Option("--role-arn <arn>", "IAM role ARN to assume for AWS API calls (env: CDKD_ROLE_ARN); the role needs the actions for the resource types you deploy plus the bookkeeping actions cdkd itself issues, because cdkd makes raw service API calls and does not route through CloudFormation, so CDK CLI deploy-roles will NOT work. Do not combine with --profile: the SDK then ignores the assumed role"),
250
250
  new Option("-y, --yes", "Automatically answer interactive prompts with the recommended response (e.g. confirm destroy)").default(false)
251
251
  ];
252
252
  /**
@@ -23208,7 +23208,43 @@ function deepEqual$1(a, b) {
23208
23208
  //#endregion
23209
23209
  //#region src/provisioning/providers/logs-loggroup-provider.ts
23210
23210
  /**
23211
- * Refuse a `RetentionInDays` the send path cannot USE (issue [#2521]).
23211
+ * Whether a raw `RetentionInDays` is CloudFormation's spelling of "no
23212
+ * retention" — the shapes the send path SKIPS on create and routes to
23213
+ * `DeleteRetentionPolicy` on update, rather than refusing.
23214
+ *
23215
+ * MEASURED, not read off the docs (issue
23216
+ * [#2699](https://github.com/go-to-k/cdkd/issues/2699), live CloudFormation
23217
+ * A/B on `AWS::Logs::LogGroup`, us-east-1, 2026-09-14; the full table is on
23218
+ * `toCfnInteger` in `../dynamodb-warm-throughput.ts`): an ABSENT property, an
23219
+ * EMPTY string and a WHITESPACE-ONLY string are all accepted by CloudFormation
23220
+ * and all REMOVE the live retention policy. Nothing else in the falsy family
23221
+ * is: `false`, `0`, `"0"` and `null` are every one REJECTED (the first three by
23222
+ * the Logs handler, `null` at `update-stack` itself), so routing them here
23223
+ * would make cdkd delete a live retention on a template CloudFormation refuses
23224
+ * to deploy. They go to {@link assertUsableRetention}.
23225
+ *
23226
+ * The ONE cdkd-side exception is a numeric `0` when `desiredFromAwsReadback`
23227
+ * is set. `0` is what {@link LogsLogGroupProvider.readCurrentState} records for
23228
+ * a log group with no retention policy — cdkd's OWN never-expire spelling,
23229
+ * persisted into `observedProperties` at every deploy — and `cdkd drift
23230
+ * --revert` hands `update()` that baseline as the DESIRED bag. Reverting a
23231
+ * console-added retention on a never-expiring log group is therefore desired
23232
+ * `0` over live `30`, and it must reach the delete arm; refusing it would leave
23233
+ * `--revert` unable to undo exactly the drift it reports. `UpdateContext`'s
23234
+ * own contract licenses this reading and nothing wider: the flag says every
23235
+ * value in the bag describes what AWS held, so `0` there is a report of "no
23236
+ * retention", not a template's request. On the template path `0` stays
23237
+ * refused, matching CloudFormation. `"0"` is never cdkd-produced (the readback
23238
+ * writes a number) and is refused everywhere.
23239
+ */
23240
+ function isAbsentRetention(raw, desiredFromAwsReadback) {
23241
+ if (raw === void 0) return true;
23242
+ if (typeof raw === "string" && raw.trim() === "") return true;
23243
+ return desiredFromAwsReadback && raw === 0;
23244
+ }
23245
+ /**
23246
+ * Refuse a `RetentionInDays` the send path cannot USE (issues [#2521],
23247
+ * [#2698], [#2699]).
23212
23248
  *
23213
23249
  * **The scope of this refusal is a TEST, not a paragraph.** Every review round
23214
23250
  * on issue [#2521] found a hole in a PROSE statement of it — a coercible
@@ -23225,56 +23261,72 @@ function deepEqual$1(a, b) {
23225
23261
  * not cover; do not restate the rule here, because a fourth restatement is
23226
23262
  * the thing that keeps being wrong.
23227
23263
  *
23228
- * THREE refusal CASES across TWO `throw` statements — the non-finite guard
23229
- * clause at the top of the body has its own, and the other two share the
23230
- * second, differing only in the `detail` clause. Only the WHY of each:
23231
- *
23232
- * - **a non-finite NUMBER** (`NaN`, `Infinity`, `-Infinity`). It sits ahead
23233
- * of the falsy return because `NaN` is FALSY: without it that value exits
23234
- * early, fails the `> 0` send test and reaches `DeleteRetentionPolicy`,
23235
- * while its two siblings — truthy — were already refused. Leaving one
23236
- * member of a family out is the inconsistency the negative arm below
23237
- * introduced, so the arm names the family.
23238
- * - **not a number at all** (`' '`, `'abc'`, `[]`, `{}`, `true`). The
23239
- * pre-coercion code tested `if (properties['RetentionInDays'])` and handed
23240
- * the raw value to `PutRetentionPolicyCommand`, so CloudWatch Logs rejected
23241
- * it. Refusing moves that rejection one layer earlier and names the
23242
- * property.
23264
+ * What IS stated here is the RULE the rows pin, because since issues #2698 /
23265
+ * #2699 it is a measurement rather than a guess: the value is read through
23266
+ * `toCfnInteger` — CloudFormation's own Integer grammar, `optional sign +
23267
+ * decimal digits`, surrounding whitespace trimmed — and anything that grammar
23268
+ * rejects is refused here where CloudFormation would have refused it at
23269
+ * deploy time. The pre-#2698 reader was `Number()`, which FORWARDED `'0x1e'`
23270
+ * as 30 and `'30.5'` as 30.5 on a template CloudFormation rejects outright.
23271
+ * Five refusal ARMS, each with its own `detail` clause; only the WHY of each:
23272
+ *
23273
+ * - **a non-finite NUMBER** (`NaN`, `Infinity`, `-Infinity`). Unreachable in
23274
+ * production — both bags are `JSON.parse` output, which cannot express one —
23275
+ * and kept as defence so the family is named whole. Sits BEFORE the null /
23276
+ * type arms because `NaN` is a number whose siblings were already refused.
23277
+ * - **`null`**. CloudFormation rejects it at `update-stack` (`'null' values
23278
+ * are not allowed in templates`), and cdkd's own resolver never
23279
+ * manufactures one for this key: an `AWS::NoValue` — bare or as an `Fn::If`
23280
+ * branch — OMITS the property (`intrinsic-function-resolver.ts`,
23281
+ * `resolveValue`'s object arm), so `null` arriving here is a literal the
23282
+ * template wrote, not cdkd's spelling of absent. Issue #2521 left it in the
23283
+ * delete arm on the guess that it was; the measurement settled it.
23284
+ * - **not a CloudFormation Integer at all** (`'abc'`, `'0x1e'`, `'1e3'`,
23285
+ * `'30.5'`, `[]`, `{}`, `true`, `false`). The pre-coercion code handed the
23286
+ * raw value to `PutRetentionPolicyCommand` and CloudWatch Logs rejected it;
23287
+ * refusing moves that rejection one layer earlier and names the property.
23288
+ * `false` joined this arm with #2699 — it was in the delete arm before, on
23289
+ * the same guess as `null`.
23290
+ * - **ZERO** (`0`, `'0'`). The Logs handler rejects it by enum (`Valid values
23291
+ * are: [1, 3, 5, ...]`), so it is not CloudFormation's never-expire — the
23292
+ * ABSENT property or an empty string is. Numeric `0` under
23293
+ * `desiredFromAwsReadback` never reaches this arm; see
23294
+ * {@link isAbsentRetention} for why that one is cdkd's own spelling.
23243
23295
  * - **a NEGATIVE number** (`-1`, `'-1'`). It coerces, so a refusal gated on
23244
23296
  * coercibility alone let it through; it then failed the `> 0` send test and
23245
23297
  * fell into the UPDATE path's `else`, which issues
23246
23298
  * `DeleteRetentionPolicy` — a template typo SILENTLY REMOVING a live
23247
23299
  * retention where the old code sent `-1` and failed loudly.
23248
23300
  *
23249
- * The FALSY values pass through, as they did before — `0`, `''`, `false`,
23250
- * `null` and absent; `NaN` is the one exception, refused above because it is a
23251
- * malformed number rather than a spelling of "no retention".
23252
- *
23253
- * `'0'` is TRUTHY, so it reaches this function and passes too: zero is
23254
- * what {@link LogsLogGroupProvider.readCurrentState} records for a group with
23255
- * no retention policy, and CloudFormation coerces `'0'` to `0`, so routing the
23256
- * string to the same arm as the number is the consistency the coercion is for.
23257
- * On the UPDATE path against a live positive retention that is a real change —
23258
- * it DELETES where the old code sent and was rejected — and `UPDATE_MATRIX`
23259
- * pins it.
23260
- *
23261
23301
  * The message names the value's TYPE and never the value. A provider's
23262
23302
  * `properties` bag arrives with dynamic references already RESOLVED, so a
23263
23303
  * `{{resolve:secretsmanager:...}}` scalar is PLAINTEXT by the time it reaches
23264
- * here, and this function has no masker to thread — `create()` takes no
23265
- * `CreateContext` and `applyUpdate` no `UpdateContext`
23304
+ * here, and this function has no masker threaded
23266
23305
  * (`.claude/rules/provider-masking.md`). Interpolating it as
23267
23306
  * `${JSON.stringify(raw)}` was the first draft and
23268
23307
  * `vp run audit:provider-secret-mask:check` refused it — but read that as one
23269
23308
  * SPELLING being fenced, not the rule: the critic's population is
23270
23309
  * interpolated `JSON.stringify` calls, so a bare `${raw}` here would leave it
23271
23310
  * green. Do not put the value back without threading the capability first.
23311
+ *
23312
+ * The caller decides the ABSENT family FIRST, through {@link isAbsentRetention},
23313
+ * and never calls this for one: an absent value is not an error, and the
23314
+ * readback-only reading of `0` lives in that predicate rather than here, so
23315
+ * the zero arm below can stay unconditional.
23272
23316
  */
23273
23317
  function assertUsableRetention(raw, coerced, logicalId, resourceType) {
23274
- if (typeof raw === "number" && !Number.isFinite(raw)) throw new ProvisioningError(`${logicalId} (${resourceType}): RetentionInDays must be a non-negative number or a numeric string (CloudFormation coerces '30' to 30); the template declares a non-finite number`, resourceType, logicalId);
23275
- if (!raw) return;
23276
- if (coerced !== void 0 && coerced >= 0) return;
23277
- throw new ProvisioningError(`${logicalId} (${resourceType}): RetentionInDays must be a non-negative number or a numeric string (CloudFormation coerces '30' to 30); ${coerced === void 0 ? `the template's value is of type ${Array.isArray(raw) ? "array" : typeof raw} and does not parse as a finite number` : "the template declares a negative number"}`, resourceType, logicalId);
23318
+ const detail = (() => {
23319
+ if (typeof raw === "number" && !Number.isFinite(raw)) return "the template declares a non-finite number";
23320
+ if (raw === null) return "the template declares null, which CloudFormation rejects ('null' values are not allowed in templates); to declare no retention, omit the property";
23321
+ if (coerced === void 0) {
23322
+ if (typeof raw === "string") return "the template declares a string that is not a CloudFormation Integer (hex, exponent and decimal-point spellings are rejected, as they are by CloudFormation)";
23323
+ return `the template's value is of type ${Array.isArray(raw) ? "array" : typeof raw}`;
23324
+ }
23325
+ if (coerced === 0) return "the template declares zero, which CloudWatch Logs rejects; to declare no retention, omit the property or declare an empty string";
23326
+ if (coerced < 0) return "the template declares a negative number";
23327
+ })();
23328
+ if (detail === void 0) return;
23329
+ throw new ProvisioningError(`${logicalId} (${resourceType}): RetentionInDays must be a positive CloudFormation Integer (optional sign then decimal digits, as a number or a string — 30 or '30'); ${detail}`, resourceType, logicalId);
23278
23330
  }
23279
23331
  /**
23280
23332
  * AWS CloudWatch Logs LogGroup Provider
@@ -23333,7 +23385,7 @@ var LogsLogGroupProvider = class {
23333
23385
  /**
23334
23386
  * Create a CloudWatch Logs log group
23335
23387
  */
23336
- async create(logicalId, resourceType, properties) {
23388
+ async create(logicalId, resourceType, properties, context) {
23337
23389
  this.logger.debug(`Creating log group ${logicalId}`);
23338
23390
  const logGroupName = properties["LogGroupName"] || `/cdkd/${generateResourceName(logicalId, {
23339
23391
  maxLength: 506,
@@ -23358,8 +23410,17 @@ var LogsLogGroupProvider = class {
23358
23410
  }
23359
23411
  try {
23360
23412
  const rawRetention = properties["RetentionInDays"];
23361
- const retentionInDays = toFiniteNumber(rawRetention);
23362
- assertUsableRetention(rawRetention, retentionInDays, logicalId, resourceType);
23413
+ let retentionInDays = toCfnInteger(rawRetention);
23414
+ if (!isAbsentRetention(rawRetention, false)) try {
23415
+ assertUsableRetention(rawRetention, retentionInDays, logicalId, resourceType);
23416
+ } catch (refusal) {
23417
+ if (context?.replayingState !== true || !(refusal instanceof ProvisioningError)) throw refusal;
23418
+ const legacyReading = toFiniteNumber(rawRetention);
23419
+ if (legacyReading !== void 0 && Number.isInteger(legacyReading) && legacyReading > 0) {
23420
+ retentionInDays = legacyReading;
23421
+ this.logger.warn(`${refusal.message}. This create replays a cdkd state record (rollback) written by a cdkd that read the value as ${legacyReading} and applied it, so the retention is RESTORED as ${legacyReading} days rather than refused. Fix the template spelling and re-deploy.`);
23422
+ } else this.logger.warn(`${refusal.message}. This create replays a cdkd state record (rollback), so the retention is SKIPPED rather than refused: the log group is re-created with no retention policy. Re-apply it by hand (aws logs put-retention-policy) or fix the template and re-deploy.`);
23423
+ }
23363
23424
  if (retentionInDays !== void 0 && retentionInDays > 0) await this.logsClient.send(new PutRetentionPolicyCommand({
23364
23425
  logGroupName,
23365
23426
  retentionInDays
@@ -23390,7 +23451,12 @@ var LogsLogGroupProvider = class {
23390
23451
  await this.logsClient.send(new DeleteLogGroupCommand({ logGroupName }));
23391
23452
  this.logger.debug(`Cleaned up partially-created log group ${logicalId} (${logGroupName}) after wiring failure`);
23392
23453
  } catch (cleanupError) {
23393
- this.logger.warn(`Failed to clean up partially-created log group ${logicalId} (${logGroupName}): ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}. Manual deletion may be required before the next deploy: aws logs delete-log-group --log-group-name '${logGroupName}'`);
23454
+ const deleteCommand = renderDisableCommand({
23455
+ before: "aws logs delete-log-group --log-group-name",
23456
+ identifier: logGroupName
23457
+ });
23458
+ const manualStep = deleteCommand ? `Manual deletion may be required before the next deploy: ${deleteCommand}` : "Manual deletion may be required before the next deploy, via the console: the log group name cannot be reproduced safely on a command line.";
23459
+ this.logger.warn(`Failed to clean up partially-created log group ${logicalId} (${logGroupName}): ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}. ${manualStep}`);
23394
23460
  }
23395
23461
  throw innerError;
23396
23462
  }
@@ -23428,32 +23494,40 @@ var LogsLogGroupProvider = class {
23428
23494
  * fall back to replacement, so swallowing it into a ProvisioningError would
23429
23495
  * turn a recoverable class change into a hard failure.
23430
23496
  */
23431
- async update(logicalId, physicalId, resourceType, properties, previousProperties) {
23497
+ async update(logicalId, physicalId, resourceType, properties, previousProperties, context) {
23432
23498
  try {
23433
- return await this.applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties);
23499
+ return await this.applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties, context);
23434
23500
  } catch (error) {
23435
23501
  if (error instanceof CdkdError) throw error;
23436
23502
  const cause = error instanceof Error ? error : void 0;
23437
23503
  throw new ProvisioningError(`Failed to update log group ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
23438
23504
  }
23439
23505
  }
23440
- async applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties) {
23506
+ async applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties, context) {
23441
23507
  this.logger.debug(`Updating log group ${logicalId}: ${physicalId}`);
23442
23508
  const normalizeClass = (v) => typeof v === "string" && v.length > 0 ? v : "STANDARD";
23443
23509
  const prevClass = normalizeClass(previousProperties["LogGroupClass"]);
23444
23510
  const nextClass = normalizeClass(properties["LogGroupClass"]);
23445
23511
  if (prevClass !== nextClass) {
23446
23512
  const replaceFlags = "--replace --force-stateful-recreation";
23447
- const remedy = isTruthyCfnBoolean(previousProperties["DeletionProtectionEnabled"]) ? `cdkd's recorded properties for this log group carry DeletionProtectionEnabled, so ${replaceFlags} alone will NOT succeed while AWS still has it on: the replacement normally deletes the log group, AWS refuses that delete while protection is on, and cdkd deploy has no --remove-protection flag to clear it (only cdkd destroy and cdkd state destroy act on one). Read "Deletion protection blocks a replacement" in docs/cli-deploy-safety.md BEFORE you disable anything: whether disabling helps at all, and what the flag ends up as, depend on your UpdateReplacePolicy and on whether the deploy completes — neither of which this refusal can see. Then disable deletion protection — \`aws logs put-log-group-deletion-protection --log-group-identifier '${physicalId}' --no-deletion-protection-enabled\`, or via the console — and re-deploy with ${replaceFlags} to delete + recreate the log group under the new class (its stored log events are lost). Setting DeletionProtectionEnabled: false in the template does NOT clear it in the same deploy: this refusal fires before that property is applied, so that route needs its own deploy with the LogGroupClass change reverted. Or revert the LogGroupClass change and keep the current class.` : `Re-deploy with ${replaceFlags} to delete + recreate the log group under the new class (its stored log events are lost), or revert the LogGroupClass change.`;
23513
+ const deletionProtected = isTruthyCfnBoolean(previousProperties["DeletionProtectionEnabled"]);
23514
+ const disableCommand = renderDisableCommand({
23515
+ before: "aws logs put-log-group-deletion-protection --log-group-identifier",
23516
+ identifier: physicalId,
23517
+ after: "--no-deletion-protection-enabled"
23518
+ });
23519
+ const disableStep = disableCommand ? `Then disable deletion protection — \`${disableCommand}\`, or via the console — and re-deploy with ${replaceFlags}` : `${UNNAMEABLE_ID_CLAUSE} Then re-deploy with ${replaceFlags}`;
23520
+ const remedy = deletionProtected ? `cdkd's recorded properties for this log group carry DeletionProtectionEnabled, so ${replaceFlags} alone will NOT succeed while AWS still has it on: the replacement normally deletes the log group, AWS refuses that delete while protection is on, and cdkd deploy has no --remove-protection flag to clear it (only cdkd destroy and cdkd state destroy act on one). Read "Deletion protection blocks a replacement" in docs/cli-deploy-safety.md BEFORE you disable anything: whether disabling helps at all, and what the flag ends up as, depend on your UpdateReplacePolicy and on whether the deploy completes — neither of which this refusal can see. ${disableStep} to delete + recreate the log group under the new class (its stored log events are lost). Setting DeletionProtectionEnabled: false in the template does NOT clear it in the same deploy: this refusal fires before that property is applied, so that route needs its own deploy with the LogGroupClass change reverted. Or revert the LogGroupClass change and keep the current class.` : `Re-deploy with ${replaceFlags} to delete + recreate the log group under the new class (its stored log events are lost), or revert the LogGroupClass change.`;
23448
23521
  throw new ResourceUpdateNotSupportedError("AWS::Logs::LogGroup", logicalId, `the LogGroupClass ('${prevClass}' -> '${nextClass}') cannot be changed after creation. ${remedy} Note --force-stateful-recreation has NO per-resource granularity: it clears the data guard for every replacement target in the run`);
23449
23522
  }
23450
23523
  const rawRetention = properties["RetentionInDays"];
23451
23524
  const rawPreviousRetention = previousProperties["RetentionInDays"];
23452
23525
  const renderedRetention = JSON.stringify(rawRetention);
23453
23526
  const sameRawRetention = Object.is(rawRetention, rawPreviousRetention) || renderedRetention !== void 0 && renderedRetention !== "null" && renderedRetention === JSON.stringify(rawPreviousRetention);
23454
- const retentionInDays = toFiniteNumber(rawRetention);
23455
- const oldRetentionInDays = toFiniteNumber(rawPreviousRetention);
23456
- if (!sameRawRetention) assertUsableRetention(rawRetention, retentionInDays, logicalId, resourceType);
23527
+ const retentionInDays = toCfnInteger(rawRetention);
23528
+ const oldRetentionInDays = toCfnInteger(rawPreviousRetention);
23529
+ const desiredFromAwsReadback = context?.desiredFromAwsReadback === true;
23530
+ if (!sameRawRetention && !isAbsentRetention(rawRetention, desiredFromAwsReadback)) assertUsableRetention(rawRetention, retentionInDays, logicalId, resourceType);
23457
23531
  const newKmsKeyId = properties["KmsKeyId"];
23458
23532
  if (newKmsKeyId !== previousProperties["KmsKeyId"]) {
23459
23533
  if (newKmsKeyId) await this.logsClient.send(new AssociateKmsKeyCommand({
@@ -23463,7 +23537,8 @@ var LogsLogGroupProvider = class {
23463
23537
  else await this.logsClient.send(new DisassociateKmsKeyCommand({ logGroupName: physicalId }));
23464
23538
  this.logger.debug(`Updated KMS key association for log group ${physicalId}`);
23465
23539
  }
23466
- if (retentionInDays !== oldRetentionInDays || !sameRawRetention && Boolean(rawPreviousRetention) && oldRetentionInDays === void 0) {
23540
+ const previousRetentionUnknown = !sameRawRetention && Boolean(rawPreviousRetention) && !isAbsentRetention(rawPreviousRetention, false) && oldRetentionInDays === void 0;
23541
+ if (retentionInDays !== oldRetentionInDays || previousRetentionUnknown) {
23467
23542
  if (retentionInDays !== void 0 && retentionInDays > 0) await this.logsClient.send(new PutRetentionPolicyCommand({
23468
23543
  logGroupName: physicalId,
23469
23544
  retentionInDays
@@ -26663,17 +26738,18 @@ function canonicalizeSgInlineRuleProtocols(properties) {
26663
26738
  * `definedAttributes` then drops the `undefined` members; it keeps `''`, so the
26664
26739
  * pending-window normalization has to happen HERE, where the state is known.
26665
26740
  *
26666
- * The residual that follows from the pending rule: a `--no-wait` create of an
26667
- * instance in a PRIVATE subnet omits the public pair, and nothing later writes
26668
- * it -- a no-change deploy's auto-refresh touches `observedProperties`, never
26669
- * `attributes` -- so `Fn::GetAtt [.., PublicIp]` takes the resolver's live arm,
26670
- * finds no address, and degrades to the instance id with a warning until the
26671
- * next `update()` of that instance records the known-empty pair. Tracked on
26672
- * go-to-k/cdkd#3096.
26741
+ * What follows from the pending rule: a `--no-wait` create of an instance in
26742
+ * a PRIVATE subnet omits the public pair, and nothing later writes it -- a
26743
+ * no-change deploy's auto-refresh touches `observedProperties`, never
26744
+ * `attributes` -- so `Fn::GetAtt [.., PublicIp]` takes the resolver's live arm
26745
+ * on every resolution. That arm applies THIS SAME three-state rule at read
26746
+ * time (issue #3096): a settled instance's missing public member resolves to
26747
+ * the known `''`, while a still-`pending` one is REFUSED rather than degraded
26748
+ * to the instance id, and nothing is cached so the next deploy re-reads.
26673
26749
  */
26674
26750
  function describedInstanceAttributes(instanceId, instance) {
26675
26751
  const stateName = instance?.State?.Name;
26676
- const settled = stateName !== void 0 && stateName !== "pending";
26752
+ const settled = isSettledInstanceState(stateName);
26677
26753
  const publicMember = (value) => settled ? value ?? "" : value || void 0;
26678
26754
  return definedAttributes({
26679
26755
  InstanceId: instanceId,
@@ -70465,7 +70541,7 @@ async function resolveStateSecretExpressions(props, resolvers, secrets, options)
70465
70541
  return out;
70466
70542
  }
70467
70543
  if (v !== null && typeof v === "object") {
70468
- const out = {};
70544
+ const out = Object.create(null);
70469
70545
  for (const [k, val] of Object.entries(v)) out[k] = await walk(val, path === "" ? k : `${path}.${k}`);
70470
70546
  return out;
70471
70547
  }
@@ -70863,29 +70939,74 @@ function buildReadCurrentStateContext(state, excludedLogicalId) {
70863
70939
  * The read counterpart of {@link setAtPath}, and it parses the same subset:
70864
70940
  * the drift comparator only synthesizes paths through plain objects.
70865
70941
  */
70942
+ /**
70943
+ * Read a dotted path out of a bag, OWN keys only.
70944
+ *
70945
+ * Own keys, because the bag is `JSON.parse`d state, whose one exotic key is an
70946
+ * OWN `__proto__` (issue #2899's class): on a plain object a bare
70947
+ * `cursor['__proto__']` read for a segment the bag does NOT own answers with
70948
+ * `Object.prototype` — an inherited value that the `--accept` post-write check
70949
+ * below would then compare against the accepted one. `undefined` is the answer
70950
+ * "the bag has nothing here", which is what an absent segment means.
70951
+ *
70952
+ * Exported, like `setAtPath`, as a test seam only: a `__proto__` path cannot
70953
+ * reach either through the command today (the comparison chain's normalisers
70954
+ * drop the key on both sides — issue #3121), so the own-key rule is pinned on
70955
+ * the helpers directly.
70956
+ */
70866
70957
  function getAtPath(source, path) {
70867
70958
  if (path.length === 0) return source;
70868
70959
  let cursor = source;
70869
70960
  for (const segment of path.split(".")) {
70870
70961
  if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) return void 0;
70962
+ if (!hasOwnKey(cursor, segment)) return void 0;
70871
70963
  cursor = cursor[segment];
70872
70964
  }
70873
70965
  return cursor;
70874
70966
  }
70967
+ /**
70968
+ * Write a dotted path into a bag as OWN keys.
70969
+ *
70970
+ * `Object.defineProperty` rather than assignment, and a null-prototype
70971
+ * intermediate node (issue #2899's class): the target is `JSON.parse`d state
70972
+ * cloned by `runAccept`, a plain object, and a drifted top-level key literally
70973
+ * named `__proto__` (which `JSON.parse` yields as an ordinary own key and the
70974
+ * comparator enumerates like any other) would be ASSIGNED as the prototype —
70975
+ * the accepted value silently dropped from the baseline while the summary
70976
+ * counts it recorded. Defining it makes it the own data property the record
70977
+ * round-trips through `JSON.stringify`.
70978
+ */
70875
70979
  function setAtPath(target, path, value) {
70876
70980
  if (path.length === 0) return;
70877
70981
  const segments = path.split(".");
70878
70982
  let cursor = target;
70879
70983
  for (let i = 0; i < segments.length - 1; i++) {
70880
70984
  const key = segments[i];
70881
- const next = cursor[key];
70985
+ const next = ownValue(cursor, key);
70882
70986
  if (next === void 0 || next === null || typeof next !== "object" || Array.isArray(next)) {
70883
- const fresh = {};
70884
- cursor[key] = fresh;
70987
+ const fresh = Object.create(null);
70988
+ defineOwnKey(cursor, key, fresh);
70885
70989
  cursor = fresh;
70886
70990
  } else cursor = next;
70887
70991
  }
70888
- cursor[segments[segments.length - 1]] = value;
70992
+ defineOwnKey(cursor, segments[segments.length - 1], value);
70993
+ }
70994
+ /** Own-key membership: `in` reads the prototype chain, which a `JSON.parse`d bag never means. */
70995
+ function hasOwnKey(target, key) {
70996
+ return Object.prototype.hasOwnProperty.call(target, key);
70997
+ }
70998
+ /** An own-key read: `undefined` for an inherited name (`__proto__`, `constructor`, ...). */
70999
+ function ownValue(target, key) {
71000
+ return hasOwnKey(target, key) ? target[key] : void 0;
71001
+ }
71002
+ /** An ordinary (writable, enumerable, configurable) own data property — never the prototype. */
71003
+ function defineOwnKey(target, key, value) {
71004
+ Object.defineProperty(target, key, {
71005
+ value,
71006
+ writable: true,
71007
+ enumerable: true,
71008
+ configurable: true
71009
+ });
70889
71010
  }
70890
71011
  /**
70891
71012
  * `--accept`: state ← AWS.
@@ -70956,14 +71077,14 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
70956
71077
  setAtPath(newBaseline, change.path, change.awsValue);
70957
71078
  accepted.push(change);
70958
71079
  }
70959
- const redactedBaseline = redactSecretsForState(newBaseline, outcome.secrets, existing.properties ?? {}, STATE_SOURCED_READBACK_RULES);
71080
+ const redactedBaseline = redactSecretsForState(newBaseline, outcome.secrets, existing.properties ?? {}, hasObserved ? STATE_SOURCED_BASELINE_RULES : STATE_SOURCED_READBACK_RULES);
70960
71081
  let recordedChanges = 0;
70961
71082
  for (const change of accepted) {
70962
71083
  if (deepEqualUnordered(getAtPath(redactedBaseline, change.path), change.awsValue)) {
70963
71084
  recordedChanges++;
70964
71085
  continue;
70965
71086
  }
70966
- logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): '${change.path}' was NOT recorded — cdkd state holds an unresolved '{{resolve:...}}' reference at that property, and the reference wins over an accepted value. Change the template if that reference is wrong.`);
71087
+ logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): '${change.path}' was NOT recorded — after redaction cdkd state holds a different value at that property. Usually an unresolved '{{resolve:...}}' reference sits there and wins over an accepted value (change the template if that reference is wrong); a readback value cdkd cannot compare is never called recorded.`);
70967
71088
  }
70968
71089
  if (recordedChanges > 0) acceptedResourceCount++;
70969
71090
  resources[outcome.logicalId] = hasObserved ? {
@@ -71104,10 +71225,10 @@ function findRevertPreservedTagKeys(drifts, desiredProperties, awsProperties) {
71104
71225
  if (topLevelKey) driftedTopLevelKeys.add(topLevelKey);
71105
71226
  }
71106
71227
  for (const key of driftedTopLevelKeys) {
71107
- if (!(key in desiredProperties)) continue;
71228
+ if (!hasOwnKey(desiredProperties, key)) continue;
71108
71229
  if (!isTagListKey(key)) continue;
71109
71230
  const desiredValue = desiredProperties[key];
71110
- const awsValue = awsProperties[key];
71231
+ const awsValue = ownValue(awsProperties, key);
71111
71232
  if (!(isCfnTagList(desiredValue) || Array.isArray(desiredValue) && desiredValue.length === 0) || !isCfnTagList(awsValue)) continue;
71112
71233
  const baselineKeys = new Set(tagKeys(desiredValue));
71113
71234
  for (const tagKey of tagKeys(awsValue)) if (!baselineKeys.has(tagKey) && isServiceManagedTagKey(tagKey)) preserved.add(`${key}.${tagKey}`);
@@ -71163,8 +71284,8 @@ function findRevertUnbaselinedAwsKeys(drifts, desiredProperties, awsProperties)
71163
71284
  if (topLevelKey) driftedTopLevelKeys.add(topLevelKey);
71164
71285
  }
71165
71286
  for (const key of driftedTopLevelKeys) {
71166
- if (!(key in desiredProperties)) continue;
71167
- collectMissingPaths(awsProperties[key], desiredProperties[key], key, missing, isTagListKey(key) ? isServiceManagedTagKey : void 0);
71287
+ if (!hasOwnKey(desiredProperties, key)) continue;
71288
+ collectMissingPaths(ownValue(awsProperties, key), desiredProperties[key], key, missing, isTagListKey(key) ? isServiceManagedTagKey : void 0);
71168
71289
  }
71169
71290
  return [...missing].sort();
71170
71291
  }
@@ -71220,7 +71341,7 @@ function collectMissingPaths(awsValue, desiredValue, path, out, isPreservedKey)
71220
71341
  }
71221
71342
  for (const [key, value] of Object.entries(awsValue)) {
71222
71343
  const childPath = `${path}.${key}`;
71223
- if (!(key in desiredValue)) {
71344
+ if (!hasOwnKey(desiredValue, key)) {
71224
71345
  out.add(childPath);
71225
71346
  continue;
71226
71347
  }
@@ -71345,8 +71466,8 @@ function mergeUntemplatedValue(awsValue, desiredValue) {
71345
71466
  if (!isPlainRecord(awsValue) || !isPlainRecord(desiredValue)) return desiredValue;
71346
71467
  if (!hasPlainPrototype(awsValue) || !hasPlainPrototype(desiredValue)) return desiredValue;
71347
71468
  const merged = Object.create(null);
71348
- for (const [key, value] of Object.entries(awsValue)) merged[key] = key in desiredValue ? mergeUntemplatedValue(value, desiredValue[key]) : value;
71349
- for (const [key, value] of Object.entries(desiredValue)) if (!(key in awsValue)) merged[key] = value;
71469
+ for (const [key, value] of Object.entries(awsValue)) merged[key] = hasOwnKey(desiredValue, key) ? mergeUntemplatedValue(value, desiredValue[key]) : value;
71470
+ for (const [key, value] of Object.entries(desiredValue)) if (!hasOwnKey(awsValue, key)) merged[key] = value;
71350
71471
  return merged;
71351
71472
  }
71352
71473
  /**
@@ -71404,7 +71525,7 @@ function preserveLiveValuesAtUnresolvedTokens(send, awsProperties) {
71404
71525
  if (typeof value === "string" && value.includes("{{resolve:")) {
71405
71526
  if (!isSingleDynamicReferenceToken(value)) return value;
71406
71527
  if (live === void 0) return value;
71407
- return live;
71528
+ return typeof live === "string" ? live : value;
71408
71529
  }
71409
71530
  if (Array.isArray(value)) {
71410
71531
  const liveItems = pairedLiveItems(value, live, isTokenOrMaskLeaf, carriesTokenOrMask, true);
@@ -71414,7 +71535,7 @@ function preserveLiveValuesAtUnresolvedTokens(send, awsProperties) {
71414
71535
  if (!hasPlainPrototype(value)) return value;
71415
71536
  const liveObject = isPlainRecord(live) ? live : void 0;
71416
71537
  const out = Object.create(null);
71417
- for (const [k, v] of Object.entries(value)) out[k] = walk(v, liveObject?.[k]);
71538
+ for (const [k, v] of Object.entries(value)) out[k] = walk(v, liveObject === void 0 ? void 0 : ownValue(liveObject, k));
71418
71539
  return out;
71419
71540
  }
71420
71541
  return value;
@@ -71649,7 +71770,7 @@ function pairedLiveItems(send, live, wildcardLeaf = isMaskLeaf, carriesWildcard
71649
71770
  for (const item of live) byIdentity.set(item[key], item);
71650
71771
  return send.map((item) => {
71651
71772
  const identity = item[key];
71652
- return wildcardLeaf(identity) ? void 0 : byIdentity.get(identity);
71773
+ return isTokenOrMaskLeaf(identity) ? void 0 : byIdentity.get(identity);
71653
71774
  });
71654
71775
  }
71655
71776
  if (live.length !== send.length) return void 0;
@@ -71843,12 +71964,13 @@ function corroboratedLeafCount(send, live, wildcardLeaf = isMaskLeaf) {
71843
71964
  * masked VALUES the walk writes still come from `send` — only the evidence
71844
71965
  * source changes. Defaults to `send` (self-corroboration, the pre-round-4
71845
71966
  * behaviour) for a caller with no sibling pass; a structural mismatch between
71846
- * the two bags — which the token pass CAN produce, by returning the live
71847
- * CONTAINER AWS holds over a whole-token string leaf — refuses the masks
71848
- * beneath it rather than falling back to the fabricable bag. That refusal is
71849
- * a deliberate fail-closed cost, not a reachable loss: the mismatching
71850
- * subtree is live-derived and carries no mask, so nothing preservable is
71851
- * refused today.
71967
+ * the two bags refuses the masks beneath it rather than falling back to the
71968
+ * fabricable bag. Until issue #2920 the token pass COULD produce one, by
71969
+ * returning the live CONTAINER AWS holds over a whole-token string leaf; it
71970
+ * now copies only a string, so no sibling pass produces the mismatch today.
71971
+ * The refusal is KEPT as a deliberate fail-closed cost rather than retired on
71972
+ * that reachability argument: a bag that disagrees with its evidence source is
71973
+ * exactly where falling back would hand the fabricable bag back.
71852
71974
  *
71853
71975
  * Returns the input bag BY IDENTITY when it holds no mask — checked FIRST via
71854
71976
  * {@link carriesSecretMask}, so a mask-free revert (the ordinary case) pays
@@ -71884,7 +72006,7 @@ function preserveLiveValuesAtMaskedLeaves(send, awsProperties, secrets, corrobor
71884
72006
  const liveObject = isPlainRecord(live) ? live : void 0;
71885
72007
  const corrObject = isPlainRecord(corr) ? corr : void 0;
71886
72008
  const out = Object.create(null);
71887
- for (const [k, v] of Object.entries(value)) out[k] = walk(v, corrObject?.[k], liveObject?.[k], path === "" ? k : `${path}.${k}`);
72009
+ for (const [k, v] of Object.entries(value)) out[k] = walk(v, corrObject === void 0 ? void 0 : ownValue(corrObject, k), liveObject === void 0 ? void 0 : ownValue(liveObject, k), path === "" ? k : `${path}.${k}`);
71888
72010
  return out;
71889
72011
  }
71890
72012
  return value;
@@ -71955,13 +72077,13 @@ function buildRevertNewProperties(drifts, desiredProperties, awsProperties, opti
71955
72077
  for (const d of drifts) {
71956
72078
  const topLevelKey = d.path.split(".", 1)[0];
71957
72079
  if (!topLevelKey) continue;
71958
- if (topLevelKey in desiredProperties) {
72080
+ if (hasOwnKey(desiredProperties, topLevelKey)) {
71959
72081
  const desiredValue = desiredProperties[topLevelKey];
71960
- const awsValue = awsProperties[topLevelKey];
71961
- if (preserveUntemplated) result[topLevelKey] = mergeUntemplatedValue(awsValue, desiredValue);
72082
+ const awsValue = ownValue(awsProperties, topLevelKey);
72083
+ if (preserveUntemplated) defineOwnKey(result, topLevelKey, mergeUntemplatedValue(awsValue, desiredValue));
71962
72084
  else {
71963
72085
  const baselineIsTagList = isCfnTagList(desiredValue) || Array.isArray(desiredValue) && desiredValue.length === 0;
71964
- result[topLevelKey] = isTagListKey(topLevelKey) && baselineIsTagList && isCfnTagList(awsValue) ? mergeTagListForRevert(desiredValue, awsValue) : desiredValue;
72086
+ defineOwnKey(result, topLevelKey, isTagListKey(topLevelKey) && baselineIsTagList && isCfnTagList(awsValue) ? mergeTagListForRevert(desiredValue, awsValue) : desiredValue);
71965
72087
  }
71966
72088
  }
71967
72089
  }
@@ -71992,7 +72114,7 @@ function buildRevertNewProperties(drifts, desiredProperties, awsProperties, opti
71992
72114
  * too. That is represented by an explicit `undefined` value, which the caller
71993
72115
  * turns into a `delete` — a plain `{...baseline, ...delta}` spread would leave
71994
72116
  * an `undefined`-valued key in the JSON instead. Presence is decided by
71995
- * `key in effective`, NOT by comparing against `undefined`: a provider that
72117
+ * OWN-key membership in `effective`, NOT by comparing against `undefined`: a provider that
71996
72118
  * delivers an explicit `undefined` and one that omits the key both mean "AWS
71997
72119
  * does not hold it", while a key whose value genuinely IS `null` on both sides
71998
72120
  * must not read as a drop.
@@ -72005,17 +72127,34 @@ function buildRevertNewProperties(drifts, desiredProperties, awsProperties, opti
72005
72127
  * prevent.
72006
72128
  */
72007
72129
  function collectNarrowedTopLevelKeys(sent, effective) {
72008
- const delta = {};
72130
+ const delta = Object.create(null);
72009
72131
  for (const key of /* @__PURE__ */ new Set([...Object.keys(sent), ...Object.keys(effective)])) {
72010
- const inSent = key in sent;
72011
- const inEffective = key in effective;
72132
+ const inSent = hasOwnKey(sent, key);
72133
+ const inEffective = hasOwnKey(effective, key);
72012
72134
  if (inSent && inEffective && deepEqualUnordered(sent[key], effective[key])) continue;
72013
72135
  if (!inSent && !inEffective) continue;
72014
72136
  delta[key] = inEffective ? effective[key] : void 0;
72015
72137
  }
72016
72138
  return delta;
72017
72139
  }
72018
- /** Deep equality that does not depend on object key ORDER. */
72140
+ /**
72141
+ * Deep equality that does not depend on object key ORDER.
72142
+ *
72143
+ * A NON-PLAIN object (`Date`, `Uint8Array`, `Map`, a class instance) on either
72144
+ * side compares UNEQUAL unless it is the same object (issue
72145
+ * [#2897](https://github.com/go-to-k/cdkd/issues/2897)): its own enumerable
72146
+ * keys are `[]`, so the key walk below reported any two `Date`s equal —
72147
+ * `0 === 0` — which HID a narrowing in {@link collectNarrowedTopLevelKeys}
72148
+ * (the changed value never reached `observedProperties`, leaving the baseline
72149
+ * stale there) and SUPPRESSED `runAccept`'s "was NOT recorded" warning. Same
72150
+ * guard, same reason as `secret-redaction.ts`'s `deepEqualJsonValue` and the
72151
+ * corroboration count above. Reachable: `sent` descends from
72152
+ * `buildRevertNewProperties`, which copies `readCurrentState`'s RAW SDK return
72153
+ * with no JSON round-trip, and `effective` is a provider's own echo. The
72154
+ * direction is deliberate — a value cdkd cannot compare is a value it must not
72155
+ * call unchanged, and for the narrowing that means the provider's echo is
72156
+ * recorded (the truthful answer) rather than silently kept stale.
72157
+ */
72019
72158
  function deepEqualUnordered(a, b) {
72020
72159
  if (a === b) return true;
72021
72160
  if (a == null || b == null) return a === b;
@@ -72024,6 +72163,7 @@ function deepEqualUnordered(a, b) {
72024
72163
  if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
72025
72164
  return a.every((v, i) => deepEqualUnordered(v, b[i]));
72026
72165
  }
72166
+ if (!hasPlainPrototype(a) || !hasPlainPrototype(b)) return false;
72027
72167
  const ao = a;
72028
72168
  const bo = b;
72029
72169
  const ak = Object.keys(ao);
@@ -72184,7 +72324,7 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
72184
72324
  try {
72185
72325
  if (updateResult?.effectiveProperties) {
72186
72326
  const delta = collectNarrowedTopLevelKeys(newProperties, updateResult.effectiveProperties);
72187
- if (Object.keys(delta).length > 0) narrowedByLogicalId.set(outcome.logicalId, redactSecretsForState(delta, secrets, revertBaseline, STATE_SOURCED_READBACK_RULES));
72327
+ if (Object.keys(delta).length > 0) narrowedByLogicalId.set(outcome.logicalId, redactSecretsForState(delta, secrets, revertBaseline, stateResource.observedProperties !== void 0 ? STATE_SOURCED_BASELINE_RULES : STATE_SOURCED_READBACK_RULES));
72188
72328
  }
72189
72329
  } catch (captureErr) {
72190
72330
  logger.warn(` ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): reverted, but the provider's reported effective properties could not be read — ${maskSecretsInText(captureErr instanceof Error ? captureErr.message : String(captureErr), secrets)}`);
@@ -72211,14 +72351,14 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
72211
72351
  const newBaseline = JSON.parse(JSON.stringify(baselineSource ?? {}));
72212
72352
  let changed = false;
72213
72353
  for (const [key, value] of Object.entries(delta)) {
72214
- if (!Object.prototype.hasOwnProperty.call(newBaseline, key)) continue;
72354
+ if (!hasOwnKey(newBaseline, key)) continue;
72215
72355
  if (value === void 0) {
72216
72356
  delete newBaseline[key];
72217
72357
  changed = true;
72218
72358
  continue;
72219
72359
  }
72220
72360
  if (!hasObserved) continue;
72221
- newBaseline[key] = value;
72361
+ defineOwnKey(newBaseline, key, value);
72222
72362
  changed = true;
72223
72363
  }
72224
72364
  if (!changed) continue;
@@ -74274,11 +74414,25 @@ async function resolveIdentifierValue(entry, ctx, deps) {
74274
74414
  try {
74275
74415
  return entry.resolve(ctx);
74276
74416
  } catch (stateOnlyError) {
74277
- if (!entry.backfill || !deps) throw stateOnlyError;
74278
- return await entry.backfill(ctx, deps);
74417
+ if (entry.backfill && deps) return await entry.backfill(ctx, deps);
74418
+ if (carriesSecretMask(ctx.attributes[entry.field])) throw new Error(maskedIdentifierAttributeReason(entry.field, ctx.logicalId));
74419
+ throw stateOnlyError;
74279
74420
  }
74280
74421
  }
74281
74422
  /**
74423
+ * The refusal for a recorded identifier attribute that holds the redaction
74424
+ * mask (issue [#2932](https://github.com/go-to-k/cdkd/issues/2932)) — the
74425
+ * `attributes` twin of the `properties` blocker in {@link buildImportPlan},
74426
+ * scoped to the ONE position `cdkd export` reads out of that bag rather than
74427
+ * to the whole bag. Widening it to any masked attribute would block every
74428
+ * Cloud-Control-imported record permanently: since issue #2847 that import
74429
+ * masks each writable model key BY DESIGN, so the population exports fine
74430
+ * today and would never stop being blocked by a re-import.
74431
+ */
74432
+ function maskedIdentifierAttributeReason(field, logicalId) {
74433
+ return `cdkd state holds only the redaction mask ('***') at attributes.${field} for '${logicalId}', and that attribute is the value cdkd export reads as this resource type's CloudFormation import identifier (${field}); nothing masked may reach the exported template, since CloudFormation would either refuse it at IMPORT or write it onto the live resource at the next update. The mask is what 'cdkd import' writes for a Cloud Control model key it could not certify as read-only — every key when cloudformation:DescribeType was unavailable. Re-deploying does NOT clear it. Repair the record with 'cdkd import <stack> --resource ${logicalId}=<physicalId> --force' after granting cloudformation:DescribeType, or export the stack without this resource and adopt it into CloudFormation by hand. See https://github.com/go-to-k/cdkd/issues/2932.`;
74434
+ }
74435
+ /**
74282
74436
  * Build the {@link CompositePhysicalIdIdentifier} for a type whose CFn
74283
74437
  * identifier is an ARN cdkd records in `attributes` under the SAME name as the
74284
74438
  * `primaryIdentifier` field.
@@ -75151,6 +75305,14 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
75151
75305
  continue;
75152
75306
  }
75153
75307
  const propertiesOverlay = resolved.propertiesOverlay ?? resolved.resourceIdentifier;
75308
+ if (carriesSecretMask(resolved.resourceIdentifier) || carriesSecretMask(propertiesOverlay)) {
75309
+ blocked.push({
75310
+ logicalId,
75311
+ resourceType,
75312
+ reason: `the CloudFormation import identifier cdkd resolved for this resource is the redaction mask ('***'), so the exported template would declare the mask as the resource's identity — CloudFormation would either refuse it at IMPORT or write it onto the live resource at the next update. cdkd state holds only the mask where the identifier should be (a masked attribute, or a masked physical id). Repair the record ('cdkd import <stack> --resource ${logicalId}=<physicalId> --force', granting cloudformation:DescribeType first if the import warned that it could not read the schema), or export the stack without this resource and adopt it into CloudFormation by hand. See https://github.com/go-to-k/cdkd/issues/2932.`
75313
+ });
75314
+ continue;
75315
+ }
75154
75316
  const unrepresentable = unrepresentableOverlayFields(raw, propertiesOverlay);
75155
75317
  if (unrepresentable.length > 0) {
75156
75318
  for (const u of unrepresentable) blocked.push({
@@ -78522,10 +78684,14 @@ function snapshotNote(resourceType, effectiveProvisionedBy, skipFinalSnapshot) {
78522
78684
  * worse than a forged diagnostic: the plan preview below is what the user
78523
78685
  * CONFIRMS against, so a planted journal could forge the plan rows themselves.
78524
78686
  *
78525
- * The ASCII allowlist, because every value it guards has a known charset: a
78687
+ * `displayIdent`, because every value it guards has a known charset: a
78526
78688
  * logical id, a CFn resource type, a change type, a run id, a segment reason,
78527
- * a stack name, an AWS region. Call it for those; `grep safe(` answers the
78528
- * scope and this comment does not.
78689
+ * a stack name, an AWS region. The ASCII allowlist, a length cap, and -- since
78690
+ * issue #3092 -- a visible boundary: an all-ASCII id such as
78691
+ * `X (AWS::RDS::DBInstance) -- already reverted` survives the allowlist and
78692
+ * plants this preview's own annotation wording inside a real row, so a value
78693
+ * that is not a plain identifier renders JSON-quoted. Call it for those;
78694
+ * `grep safe(` answers the scope and this comment does not.
78529
78695
  *
78530
78696
  * NOT for a value that is about to be USED rather than shown -- the preview
78531
78697
  * indexes `previewState` by the RAW `op.logicalId`, and sanitising a lookup
@@ -78538,7 +78704,7 @@ function snapshotNote(resourceType, effectiveProvisionedBy, skipFinalSnapshot) {
78538
78704
  * answers how many; a count written here was wrong on its first revision.
78539
78705
  */
78540
78706
  function safe(value) {
78541
- return displaySafe(value, { asciiOnly: true }) || "<unrenderable>";
78707
+ return displayIdent(value);
78542
78708
  }
78543
78709
  /**
78544
78710
  * Human label for a planned rollback action (plan preview). `skipFinalSnapshot`
@@ -92291,4 +92457,4 @@ function buildProgram() {
92291
92457
 
92292
92458
  //#endregion
92293
92459
  export { buildProgram };
92294
- //# sourceMappingURL=program-z-ESK5sd.js.map
92460
+ //# sourceMappingURL=program-20PIJFu9.js.map