@go-to-k/cdkd 0.290.1 → 0.290.3

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-aYUEGRRl.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-0Wg4MwP2.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-DTli1mA9.js";
5
- import { r as protectedReplacementAdvice, t as ASGProvider } from "./asg-provider-e05iqtU_.js";
4
+ import { t as getCdkdVersion } from "./version-BTao4oxo.js";
5
+ import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-RLnpBIKj.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";
@@ -26,9 +26,9 @@ import { AddTagsToResourceCommand as AddTagsToResourceCommand$1, CreateCacheClus
26
26
  import { ACMClient, AddTagsToCertificateCommand, DeleteCertificateCommand, DescribeCertificateCommand, ListTagsForCertificateCommand, RemoveTagsFromCertificateCommand, RequestCertificateCommand, ResourceNotFoundException as ResourceNotFoundException$7, UpdateCertificateOptionsCommand } from "@aws-sdk/client-acm";
27
27
  import { CreateMicrovmImageCommand, DeleteMicrovmImageCommand, GetMicrovmImageCommand, LambdaMicrovmsClient, ListTagsCommand as ListTagsCommand$1, ResourceNotFoundException as ResourceNotFoundException$8, TagResourceCommand as TagResourceCommand$10, UntagResourceCommand as UntagResourceCommand$10, UpdateMicrovmImageCommand } from "@aws-sdk/client-lambda-microvms";
28
28
  import * as fs from "node:fs";
29
- import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
29
+ import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
30
30
  import * as path from "node:path";
31
- import { basename, dirname, isAbsolute, resolve } from "node:path";
31
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
32
32
  import { execFile, spawn } from "node:child_process";
33
33
  import { tmpdir } from "node:os";
34
34
  import { AssociateVPCWithHostedZoneCommand, ChangeResourceRecordSetsCommand, ChangeTagsForResourceCommand, CreateHostedZoneCommand, CreateQueryLoggingConfigCommand, DeleteHostedZoneCommand, DeleteQueryLoggingConfigCommand, DisassociateVPCFromHostedZoneCommand, GetHostedZoneCommand, HostedZoneAlreadyExists, ListHostedZonesByNameCommand, ListQueryLoggingConfigsCommand, ListResourceRecordSetsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$8, Route53Client, UpdateHostedZoneCommentCommand, UpdateHostedZoneFeaturesCommand } from "@aws-sdk/client-route-53";
@@ -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`
@@ -83846,7 +84012,7 @@ function resolveAssetCodePath$1(stack, logicalId, resource) {
83846
84012
  * **Order is preserved**: `Properties.Layers` is iterated left-to-right
83847
84013
  * and the resulting `ResolvedLambdaLayer[]` carries the same order. The
83848
84014
  * caller (`local-invoke.ts`'s `materializeLambdaLayers` and
83849
- * `local-start-api.ts`'s server-boot pre-merge) `cpSync`-merges every
84015
+ * `local-start-api.ts`'s server-boot pre-merge) merges every
83850
84016
  * entry into one host tmpdir in template order to honor AWS's
83851
84017
  * "last-layer-wins" file-collision semantics — Docker rejects multiple
83852
84018
  * bind mounts at the same target so cdkd cannot rely on overlay
@@ -84117,6 +84283,110 @@ function notFoundError$1(target, stack, resources) {
84117
84283
  return new LocalInvokeResolutionError(msg.trimEnd());
84118
84284
  }
84119
84285
 
84286
+ //#endregion
84287
+ //#region src/local/layer-tree-copy.ts
84288
+ /**
84289
+ * Merge one Lambda Layer's asset tree into the directory that will be
84290
+ * bind-mounted at `/opt`, with AWS's "last layer wins" semantic and with
84291
+ * symlinks kept VERBATIM (issue #3106).
84292
+ *
84293
+ * Why this is a hand-written walk and not `cpSync`. The merge used to be
84294
+ * `cpSync(src, dest, { recursive: true, force: true })`, and every
84295
+ * `cpSync`-shaped repair of it was measured wrong in some direction:
84296
+ *
84297
+ * - Without `verbatimSymlinks: true` (the default), `cpSync` rewrites a
84298
+ * RELATIVE link target to the ABSOLUTE path of the source on the host —
84299
+ * `bin/rel-link -> real.sh` arrives as `-> <cdk.out>/asset.<hash>/bin/
84300
+ * real.sh`, which is dangling inside the container, where only `dest`
84301
+ * is mounted. Measured on Node 22.12 / 24.21.
84302
+ * - WITH `verbatimSymlinks: true`, Node's C++ `cpSync` (every release
84303
+ * after 22.12) throws `EEXIST` when a later layer carries a symlink at a
84304
+ * path an earlier layer already placed one — `nodejs/node_modules/.bin/
84305
+ * <tool>` in two layers built from the same dependency is the ordinary
84306
+ * case. Measured by the go-to-k/cdkd#3118 review on 22.23 / 24.15 /
84307
+ * 24.21; the JS implementation on 22.12 does not throw, which is why
84308
+ * the floor alone did not show it.
84309
+ * - A `cpSync` + `readdirSync({ recursive: true })` pair was worse:
84310
+ * the recursive readdir DESCENDS INTO directory symlinks on every Node
84311
+ * after 22.12 (not on 22.12), so a cyclic link (`sub/up -> ..`, or a
84312
+ * pnpm-style `node_modules`) hung the merge, and an absolute link to a
84313
+ * host directory made the walk delete and write INSIDE that host
84314
+ * directory. And `cpSync`'s `force` writes THROUGH a destination that
84315
+ * is already a directory symlink, so a later layer's real directory
84316
+ * landed in the earlier layer's link target — on the host, when the
84317
+ * link was absolute. Same review, same versions.
84318
+ *
84319
+ * So the walk is explicit and recurses only on a real directory (`d_type`,
84320
+ * via `Dirent.isDirectory()`, which reports a directory symlink as a
84321
+ * symlink on 22.12 and 24.21 alike). Per entry, "last layer wins" is applied
84322
+ * the same way for every kind: whatever sits at the destination path that is
84323
+ * not of the same kind is removed first — a link, a file, or a whole
84324
+ * directory an earlier layer placed — then the entry lands. A symlink is
84325
+ * recreated with the SOURCE's own target string and never resolved, so an
84326
+ * absolute link stays absolute and a dangling one stays dangling, exactly as
84327
+ * AWS extracts the layer ZIP into `/opt`; nothing is ever written through a
84328
+ * link. A file is copied with `cpSync` (single file, `force`), which is what
84329
+ * preserves the mode bits, so a layer's `bin/<script>` keeps its `+x` — the
84330
+ * property the `local-invoke-layers` fixture executes through.
84331
+ *
84332
+ * The ROOT may itself be a symlink (an asset dir handed over through a link
84333
+ * passes `resolveAssetCodePath`'s `statSync`). `readdirSync` follows a root
84334
+ * link by itself, so the `realpathSync` is not what makes that case work,
84335
+ * and it does not move where a dangling or looping root fails either (the
84336
+ * first `readdirSync` raises the same `ENOENT` / `ELOOP` naming the asset
84337
+ * path); it only keeps every path the walk joins real.
84338
+ *
84339
+ * Bound, stated rather than handled: a FIFO or a socket inside a layer
84340
+ * makes the single-file `cpSync` throw (`ERR_FS_CP_FIFO_PIPE` /
84341
+ * `ERR_FS_CP_SOCKET`, immediately, never blocking) where the old recursive
84342
+ * copy skipped it in silence on some releases. CDK's own asset staging
84343
+ * cannot stage such an entry, so no real layer carries one.
84344
+ *
84345
+ * Both layer merges (`cdkd local invoke` and `cdkd local start-api`) call
84346
+ * this; keep them on the one helper rather than re-spelling copy options.
84347
+ */
84348
+ function copyLayerTreeLastWins(src, dest) {
84349
+ mkdirSync(dest, { recursive: true });
84350
+ copyDirLastWins(realpathSync(src), dest);
84351
+ }
84352
+ function copyDirLastWins(fromDir, toDir) {
84353
+ for (const entry of readdirSync(fromDir, { withFileTypes: true })) {
84354
+ const from = join(fromDir, entry.name);
84355
+ const to = join(toDir, entry.name);
84356
+ if (entry.isSymbolicLink()) {
84357
+ removeUnless(to, "symlink");
84358
+ symlinkSync(readlinkSync(from), to);
84359
+ } else if (entry.isDirectory()) {
84360
+ removeUnless(to, "directory");
84361
+ mkdirSync(to, { recursive: true });
84362
+ copyDirLastWins(from, to);
84363
+ } else {
84364
+ removeUnless(to, "file");
84365
+ cpSync(from, to, { force: true });
84366
+ }
84367
+ }
84368
+ }
84369
+ /**
84370
+ * Clear the destination path unless what is there is already of `kind`. A
84371
+ * directory is kept so a later layer MERGES into it; a file is kept because
84372
+ * `cpSync` overwrites it in place; a symlink is always replaced (its target
84373
+ * string comes from the later layer).
84374
+ */
84375
+ function removeUnless(to, kind) {
84376
+ let st;
84377
+ try {
84378
+ st = lstatSync(to);
84379
+ } catch {
84380
+ return;
84381
+ }
84382
+ if (kind === "directory" && st.isDirectory()) return;
84383
+ if (kind === "file" && st.isFile()) return;
84384
+ rmSync(to, {
84385
+ recursive: true,
84386
+ force: true
84387
+ });
84388
+ }
84389
+
84120
84390
  //#endregion
84121
84391
  //#region src/local/ecs-task-resolver.ts
84122
84392
  function detectEcsImageResolutionNeeds(stack) {
@@ -87533,12 +87803,12 @@ async function resolveLocalBuildPlan$1(lambda) {
87533
87803
  * when the entry is a same-stack asset. Literal-ARN entries always
87534
87804
  * pre-materialize first.
87535
87805
  * - 2+ layers → copy each into a fresh tmpdir IN ORDER (later
87536
- * layers overwrite earlier files via `cpSync({force: true})`),
87806
+ * layers overwrite earlier files via `copyLayerTreeLastWins`),
87537
87807
  * bind-mount the tmpdir at `/opt`. Records the tmpdir in
87538
87808
  * `layerTmpDirs` so `shutdown(...)` removes it.
87539
87809
  *
87540
87810
  * Issue #448: literal-ARN entries (`{kind: 'arn', ...}`) are downloaded
87541
- * + unzipped via `lambda:GetLayerVersion` BEFORE the cpSync-merge
87811
+ * + unzipped via `lambda:GetLayerVersion` BEFORE the merge
87542
87812
  * branches run. Every per-ARN tmpdir is also recorded in `layerTmpDirs`
87543
87813
  * so the same shutdown path cleans it up — even for the single-layer
87544
87814
  * fast path that bind-mounts the dir directly.
@@ -87568,10 +87838,15 @@ async function materializeLambdaLayers$1(layers, layerTmpDirs, layerRoleArn) {
87568
87838
  }
87569
87839
  if (flat.length === 1) return flat[0].assetPath;
87570
87840
  const dir = mkdtempSync(path.join(tmpdir(), "cdkd-local-start-api-layers-"));
87571
- for (const layer of flat) cpSync(layer.assetPath, dir, {
87572
- recursive: true,
87573
- force: true
87574
- });
87841
+ try {
87842
+ for (const layer of flat) copyLayerTreeLastWins(layer.assetPath, dir);
87843
+ } catch (error) {
87844
+ rmSync(dir, {
87845
+ recursive: true,
87846
+ force: true
87847
+ });
87848
+ throw error;
87849
+ }
87575
87850
  layerTmpDirs.add(dir);
87576
87851
  return dir;
87577
87852
  }
@@ -91522,7 +91797,7 @@ async function resolveZipImagePlan(lambda, options) {
91522
91797
  * - Stage 2: the resulting `{logicalId, assetPath}[]` list (in
91523
91798
  * template order — ARN entries surface their `arn` as the
91524
91799
  * `logicalId` for log lines) is handed to the existing
91525
- * `materializeLambdaLayers` `cpSync`-merge path. AWS's "last layer
91800
+ * `materializeLambdaLayers` merge path. AWS's "last layer
91526
91801
  * wins" file-collision semantic is preserved across both layer
91527
91802
  * kinds because the merge step is unchanged.
91528
91803
  */
@@ -91590,7 +91865,7 @@ function resolveTmpfsForLambda(lambda) {
91590
91865
  * than copying since CDK has already unzipped the asset.
91591
91866
  * 3. **Multiple layers**: copies each layer's contents into a fresh
91592
91867
  * tmpdir IN ORDER (later layers overwrite earlier files via
91593
- * `cpSync({force: true})`), then bind-mounts the merged tmpdir at
91868
+ * `copyLayerTreeLastWins`), then bind-mounts the merged tmpdir at
91594
91869
  * `/opt`. Returns `{ mount, tmpDir: <path> }` so the caller can
91595
91870
  * `rmSync` the tmpdir on cleanup.
91596
91871
  *
@@ -91607,10 +91882,15 @@ function materializeLambdaLayers(layers) {
91607
91882
  readOnly: true
91608
91883
  } };
91609
91884
  const tmpDir = mkdtempSync(path.join(tmpdir(), "cdkd-local-invoke-layers-"));
91610
- for (const layer of layers) cpSync(layer.assetPath, tmpDir, {
91611
- recursive: true,
91612
- force: true
91613
- });
91885
+ try {
91886
+ for (const layer of layers) copyLayerTreeLastWins(layer.assetPath, tmpDir);
91887
+ } catch (error) {
91888
+ rmSync(tmpDir, {
91889
+ recursive: true,
91890
+ force: true
91891
+ });
91892
+ throw error;
91893
+ }
91614
91894
  return {
91615
91895
  mount: {
91616
91896
  hostPath: tmpDir,
@@ -92177,4 +92457,4 @@ function buildProgram() {
92177
92457
 
92178
92458
  //#endregion
92179
92459
  export { buildProgram };
92180
- //# sourceMappingURL=program-CnTfNj2k.js.map
92460
+ //# sourceMappingURL=program-CoRRfEe_.js.map