@go-to-k/cdkd 0.290.27 → 0.290.28

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 secretBearing, $n as recordMaskOnlyValue, $r as getDockerImageBySourceHash, $t as s3BucketArn, A as extractDeploymentEventError, Ai as resetAwsClients, An as refuseMalformedState, Ar as assertAssetBucketRegion, At as isImpossibleEmptyStoredAttribute, B as toCfnInteger, Bi as LocalInvokeBuildError$1, Bn as STATE_SOURCED_BASELINE_RULES, Br as describeDockerCapturedOutput, Bt as coerceCfnBoolean, C as buildFinalSnapshotIdentifier, Ci as derivePartitionAndUrlSuffix, Cn as producerRecordKey, Cr as WorkGraph, Ct as isTerminationProtectionPropagationError, D as refusesFinalSnapshot, Di as resolveBucketRegion, Dn as refuseMalformedOutputsForDestroy, Dr as rewriteTemplateAssetReferences, Dt as cfnRefValueFromPhysicalId, E as isFinalSnapshotError, En as refuseMalformedOutputs, Er as loadPublishableAssetManifest, Et as carriesFetchableDynamicReference, F as isStatefulRecreateTargetSync, Fi as CrossAccountSecretRefusalError, Fn as findSilentDropProperties, Fr as readBootstrapMarkerBody, Ft as isSettledInstanceState, G as gray, Gi as PartialFailureError, Gn as createSecretMasker, Gr as getDockerCmd, Gt as readConfigString, H as formatResourceLine, Hn as STATE_SOURCED_READBACK_RULES, Hr as describeDockerFailure, Ht as configBooleanRefusal, I as renderStatefulReason, In as describeTypeWithThrottleRetry, Ir as validateAssetBucketName, It as WAFv2WebACLProvider, J as yellow, Ji as ResourceUpdateNotSupportedError, Jn as identityKeyFor, Jr as runDockerForeground, Jt as requireConfigObject, K as green, Ki as ProvisioningError, Kn as dynamicReferenceTokens, Kr as partitionSensitiveEnv, Kt as replayWarn, L as WARM_THROUGHPUT_MEMBERS, Li as DeployCancelledError, Ln as withRetry, Lr as validateContainerRepoName, Lt as normalizeAwsTagsToCfn, M as planOrphanAdoption, Mn as repairMalformedResourcePropertiesForReadOnly, Mr as getBootstrapMarkerKey, Mt as isUnboundTemplateParameter, N as MULTI_REGION_RECREATE_BLOCKED_TYPES, Ni as CdkdError, Nn as repairMalformedResourcesForReadOnly, Nr as isCrossRegionRedirect, Nt as parameterTypeMayLoseSecretIdentity, O as unsupportedFinalSnapshotError, Oi as AwsClients, On as refuseMalformedResourcePropertiesForOrphan, Or as AssetModeResolver, Ot as coerceParameterTypedValue, P as isStatefulRecreateTargetForReplace, Pn as findActionableSilentDrops, Pr as parseBootstrapMarker, Pt as refStateLookupFromResource, Q as isExportAliasCollision, Qn as maskSecretsInText, Qr as AssetManifestLoader, Qt as withSharedDrainBudget, R as coerceWarmThroughput$1, Ri as DynamicReferenceRegionAmbiguousError, Rn as DagBuilder, Rr as buildDenyExternalAccessPolicy, Rt as resolveExplicitPhysicalId, S as PRE_DELETE_SNAPSHOT_TYPES, Si as canonicalizeRegion, Sn as malformedStateRefusalMessage, Sr as stringifyValue, St as disableInstanceApiTermination, T as createPreDeleteFinalSnapshot, Ti as processStackMessages, Tn as refuseMalformedNestedChildOutputs, Tr as createAssetRedirectResolver, Tt as carriesDynamicReference, U as bold, Ui as MalformedProducerRecordRefusalError, Un as TEMPLATE_SOURCED_RULES, Ur as dockerSpawnEnvWithSensitive, Ut as configIntegerRefusal, V as toFiniteNumber, Vi as LocalStartServiceError, Vn as STATE_SOURCED_CROSS_GENERATION_RULES, Vr as describeDockerExecFailure, Vt as coerceCfnInteger, W as cyan, Wi as NestedStackChildDirectDestroyError, Wn as carriesSecretMask, Wr as formatDockerLoginError, Wt as configStringRefusal, X as collectPublishedOutputNames, Xi as StackTerminationProtectionError, Xn as markSameGenerationBag, Xr as escapeRegExp, Xt as classifyReplaySecretRegion, Y as collectDeclaredOutputNames, Yi as StackHasActiveImportsError, Yn as isSingleDynamicReferenceToken, Yr as runDockerStreaming, Yt as requireConfigString, Z as exportAliasCollisionScrubWarning, Zn as maskSecretsInError, Zr as stripControlChars, Zt as producerRegionsFromState, _ as isSecretBearingReferenceString, _i as uploadCfnTemplate, _n as malformedOutputsRefusalMessage, _r as isReadableBag, _t as isWaitAbandonedError, a as DeploymentEventsStore, aa as isThrottlingError, ai as resolveAutoAssetStorage, an as AWS_CREDENTIAL_ENV_KEYS, ar as rebuildClientForBucketRegion, at as clearOnUpdateRemoval, b as bindingSkippedOutputs, bi as expectedOwnerParam, bn as malformedResourcePropertiesWarning, br as shouldRetainResource, bt as deleteIndeterminateGuards, c as replayFailedOperations, ca as markRedactedCause, ci as resolveStateBucketWithDefault, cn as envCredentialsAreAssumedRole, cr as forceQuitRecoveryClause, ct as createMaskedRetryLogger, d as updatePartialReason, da as safeStringify, di as stateBucketExistenceConfirmed, dn as STATE_RESOURCES_MALFORMED, dr as CUSTOM_RESOURCE_RESPONSE_PREFIX, dt as beginCommandInterruptScope, ei as Synthesizer, en as s3BucketDomainName, er as recoverMaskedOutput, et as secretBearingStateKeyWarning, f as withResourceDeadline, fi as warnDeprecatedNoPrefixCliFlag, fn as hasReadableOutputs, fr as DEFAULT_STATE_PREFIX, ft as endCommandInterruptScope, g as bagHoldsSecretExpression, gi as findLargeInlineResources, gn as malformedLocalOutputsWarning, gr as importableOutputs, gt as CloudControlProvider, h as computeImplicitDeleteEdges, hi as MIGRATE_TMP_PREFIX, hn as malformedExportSourceWarning, hr as importableOutputKeys, ht as startInterruptWatch, i as DeploymentEventsReader, ia as isRetryableTransientError, ii as resolveApp, in as applyRoleArnIfSet, ir as S3StateBackend, it as collectInlinePolicyNamesManagedBySiblings, j as makeSiblingClaimReader, ji as setAwsClients, jn as repairMalformedOutputsForReadOnly, jr as ensureAssetStorage, jt as isNamelessDynamicReferenceError, k as makeCanonicalizePropertiesFn, ki as getAwsClients, kn as refuseMalformedResourcesForDestroy, kr as BOOTSTRAP_MARKER_PREFIX, kt as getAccountInfo, l as replayRollback, la as retryClassificationText, li as resolveStateBucketWithDefaultAndSource, ln as DiffCalculator, lr as formatLockExpiry, lt as maskDeep, m as IMPLICIT_DELETE_DEPENDENCIES, mi as CFN_TEMPLATE_URL_LIMIT, mn as malformedExportNamesWarning, mr as hasReadableExportSet, mt as isInterruptedWaitError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, na as withErrorHandling, ni as getDefaultStateBucketName, nn as s3BucketRegionalDomainName, nr as scrubResourceRecord, nt as getCurrentResourceSecrets, o as planFailedOps, oa as isTransientServerError, oi as resolveCaptureObservedState, on as applyCallerIdentityCredentials, or as buildForceUnlockCommand, ot as ProviderRegistry, p as maskingRetryLogger, pi as CFN_TEMPLATE_BODY_LIMIT, pn as hasReadableResources, pr as exportNamesCarriedFrom, pt as interruptWatchListenerCount, q as red, qi as ResourceTimeoutError, qn as errorCauseChain, qr as redactDockerArgvValues, qt as requireConfigArray, r as DeployEngine, ra as isMarkedNonRetryable, ri as getLegacyStateBucketName, rn as s3BucketWebsiteUrl, rr as LockManager, rt as IAMRoleProvider, s as planRollback, sa as markNonRetryable, si as resolveSkipPrefix, sn as callerEnvCredentials, sr as buildLockContentionMessage, st as wouldReturnToSdkProvider, t as DEFAULT_RESOURCE_TIMEOUT_MS, ta as normalizeAwsError, ti as synthesisStatusMessage, tn as s3BucketDualStackDomainName, tr as redactSecretsForState, tt as secretSafeKeyDisplay, u as updatePartialMessage, ua as describeAwsFailure, ui as resolveUseCdkBootstrapAssets, un as INTRINSIC_KEYS, ur as shellQuote, ut as maskerOrIdentity, v as keptWholeReasonText, vi as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, vn as malformedOutputsWarning, vr as orphansAfterRollback, vt as slowCcOperationTimeoutMs, w as ccRoutedFinalSnapshotError, wi as AssemblyReader, wn as refuseDivergentRecordRegionForDestroy, wr as buildAssetRedirectMap, wt as IntrinsicFunctionResolver, x as ATOMIC_FINAL_SNAPSHOT_TYPES, xi as PARTITION_TABLE, xn as malformedResourcesWarning, xr as AssetPublisher, xt as deleteSkipReason, y as mergeNoChangeOutputs, yi as decodeListingKey, yn as malformedRenderedContainersWarning, yr as orphansCarriedFrom, yt as UNSPECIFIED_SKIP_REASON, z as isWarmThroughputDecrease, zi as IntrinsicResolutionRefusalError, zn as TemplateParser, zr as buildDockerImage, zt as assertRegionMatch } from "./deploy-engine-CA70_Rji.js";
2
- import { C as displaySafe, S as displayIdent, _ as withSkipPrefix, b as STACK_REF_MAX_CODE_POINTS, 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, w as truncateCodePoints, x as UNRENDERABLE } from "./logger-CkHVgzLx.js";
1
+ import { $ as secretBearing, $n as recordMaskOnlyValue, $r as AssetManifestLoader, $t as s3BucketArn, A as extractDeploymentEventError, Ai as getAwsClients, An as refuseMalformedState, Ar as BOOTSTRAP_MARKER_PREFIX, At as isImpossibleEmptyStoredAttribute, B as toCfnInteger, Bi as IntrinsicResolutionRefusalError, Bn as STATE_SOURCED_BASELINE_RULES, Br as buildDockerImage, Bt as coerceCfnBoolean, C as buildFinalSnapshotIdentifier, Ci as canonicalizeRegion, Cn as producerRecordKey, Cr as stringifyValue, Ct as isTerminationProtectionPropagationError, D as refusesFinalSnapshot, Dn as refuseMalformedOutputsForDestroy, Dr as loadPublishableAssetManifest, Dt as cfnRefValueFromPhysicalId, E as isFinalSnapshotError, Ei as processStackMessages, En as refuseMalformedOutputs, Er as createAssetRedirectResolver, Et as carriesFetchableDynamicReference, F as isStatefulRecreateTargetSync, Fn as findSilentDropProperties, Fr as parseBootstrapMarker, Ft as isSettledInstanceState, G as gray, Gi as NestedStackChildDirectDestroyError, Gn as createSecretMasker, Gr as formatDockerLoginError, Gt as readConfigString, H as formatResourceLine, Hi as LocalStartServiceError, Hn as STATE_SOURCED_READBACK_RULES, Hr as describeDockerExecFailure, Ht as configBooleanRefusal, I as renderStatefulReason, Ii as CrossAccountSecretRefusalError, In as describeTypeWithThrottleRetry, Ir as readBootstrapMarkerBody, It as WAFv2WebACLProvider, J as yellow, Ji as ResourceTimeoutError, Jn as identityKeyFor, Jr as redactDockerArgvValues, Jt as requireConfigObject, K as green, Ki as PartialFailureError, Kn as dynamicReferenceTokens, Kr as getDockerCmd, Kt as replayWarn, L as WARM_THROUGHPUT_MEMBERS, Ln as withRetry, Lr as validateAssetBucketName, Lt as normalizeAwsTagsToCfn, M as planOrphanAdoption, Mi as setAwsClients, Mn as repairMalformedResourcePropertiesForReadOnly, Mr as ensureAssetStorage, Mt as isUnboundTemplateParameter, N as MULTI_REGION_RECREATE_BLOCKED_TYPES, Nn as repairMalformedResourcesForReadOnly, Nr as getBootstrapMarkerKey, Nt as parameterTypeMayLoseSecretIdentity, O as unsupportedFinalSnapshotError, Oi as resolveBucketRegion, On as refuseMalformedResourcePropertiesForOrphan, Or as rewriteTemplateAssetReferences, Ot as coerceParameterTypedValue, P as isStatefulRecreateTargetForReplace, Pi as CdkdError, Pn as findActionableSilentDrops, Pr as isCrossRegionRedirect, Pt as refStateLookupFromResource, Q as isExportAliasCollision, Qn as maskSecretsInText, Qr as stripControlChars, Qt as withSharedDrainBudget, R as coerceWarmThroughput$1, Ri as DeployCancelledError, Rn as DagBuilder, Rr as validateContainerRepoName, Rt as resolveExplicitPhysicalId, S as PRE_DELETE_SNAPSHOT_TYPES, Si as PARTITION_TABLE, Sn as malformedStateRefusalMessage, Sr as AssetPublisher, St as disableInstanceApiTermination, T as createPreDeleteFinalSnapshot, Ti as AssemblyReader, Tn as refuseMalformedNestedChildOutputs, Tr as buildAssetRedirectMap, Tt as carriesDynamicReference, U as bold, Un as TEMPLATE_SOURCED_RULES, Ur as describeDockerFailure, Ut as configIntegerRefusal, V as toFiniteNumber, Vi as LocalInvokeBuildError$1, Vn as STATE_SOURCED_CROSS_GENERATION_RULES, Vr as describeDockerCapturedOutput, Vt as coerceCfnInteger, W as cyan, Wi as MalformedProducerRecordRefusalError, Wn as carriesSecretMask, Wr as dockerSpawnEnvWithSensitive, Wt as configStringRefusal, X as collectPublishedOutputNames, Xi as StackHasActiveImportsError, Xn as markSameGenerationBag, Xr as runDockerStreaming, Xt as classifyReplaySecretRegion, Y as collectDeclaredOutputNames, Yi as ResourceUpdateNotSupportedError, Yn as isSingleDynamicReferenceToken, Yr as runDockerForeground, Yt as requireConfigString, Z as exportAliasCollisionScrubWarning, Zi as StackTerminationProtectionError, Zn as maskSecretsInError, Zr as escapeRegExp, Zt as producerRegionsFromState, _ as isSecretBearingReferenceString, _i as findLargeInlineResources, _n as malformedOutputsRefusalMessage, _r as importableOutputs, _t as isWaitAbandonedError, a as DeploymentEventsStore, aa as isRetryableTransientError, ai as resolveApp, an as AWS_CREDENTIAL_ENV_KEYS, ar as rebuildClientForBucketRegion, at as clearOnUpdateRemoval, b as bindingSkippedOutputs, bi as decodeListingKey, bn as malformedResourcePropertiesWarning, br as orphansCarriedFrom, bt as deleteIndeterminateGuards, c as replayFailedOperations, ca as markNonRetryable, ci as resolveSkipPrefix, cn as envCredentialsAreAssumedRole, cr as buildLockContentionMessage, ct as createMaskedRetryLogger, d as updatePartialReason, da as describeAwsFailure, di as resolveUseCdkBootstrapAssets, dn as STATE_RESOURCES_MALFORMED, dr as shellQuote, dt as beginCommandInterruptScope, ei as getDockerImageBySourceHash, en as s3BucketDomainName, er as recoverMaskedOutput, et as secretBearingStateKeyWarning, f as withResourceDeadline, fa as safeStringify, fi as stateBucketExistenceConfirmed, fn as hasReadableOutputs, fr as CUSTOM_RESOURCE_RESPONSE_PREFIX, ft as endCommandInterruptScope, g as bagHoldsSecretExpression, gi as MIGRATE_TMP_PREFIX, gn as malformedLocalOutputsWarning, gr as importableOutputKeys, gt as CloudControlProvider, h as computeImplicitDeleteEdges, hi as CFN_TEMPLATE_URL_LIMIT, hn as malformedExportSourceWarning, hr as hasReadableExportSet, ht as startInterruptWatch, i as DeploymentEventsReader, ia as isMarkedNonRetryable, ii as getLegacyStateBucketName, in as applyRoleArnIfSet, ir as S3StateBackend, it as collectInlinePolicyNamesManagedBySiblings, j as makeSiblingClaimReader, ji as resetAwsClients, jn as repairMalformedOutputsForReadOnly, jr as assertAssetBucketRegion, jt as isNamelessDynamicReferenceError, k as makeCanonicalizePropertiesFn, ki as AwsClients, kn as refuseMalformedResourcesForDestroy, kr as AssetModeResolver, kt as getAccountInfo, l as replayRollback, la as markRedactedCause, li as resolveStateBucketWithDefault, ln as DiffCalculator, lr as forceQuitRecoveryClause, lt as maskDeep, m as IMPLICIT_DELETE_DEPENDENCIES, mi as CFN_TEMPLATE_BODY_LIMIT, mn as malformedExportNamesWarning, mr as exportNamesCarriedFrom, mt as isInterruptedWaitError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, na as normalizeAwsError, ni as synthesisStatusMessage, nn as s3BucketRegionalDomainName, nr as scrubResourceRecord, nt as getCurrentResourceSecrets, o as planFailedOps, oa as isThrottlingError, oi as resolveAutoAssetStorage, on as applyCallerIdentityCredentials, or as UNREPRODUCIBLE_LOCK_CLAUSE, ot as ProviderRegistry, p as maskingRetryLogger, pi as warnDeprecatedNoPrefixCliFlag, pn as hasReadableResources, pr as DEFAULT_STATE_PREFIX, pt as interruptWatchListenerCount, q as red, qi as ProvisioningError, qn as errorCauseChain, qr as partitionSensitiveEnv, qt as requireConfigArray, r as DeployEngine, ra as withErrorHandling, ri as getDefaultStateBucketName, rn as s3BucketWebsiteUrl, rr as LockManager, rt as IAMRoleProvider, s as planRollback, sa as isTransientServerError, si as resolveCaptureObservedState, sn as callerEnvCredentials, sr as buildForceUnlockCommand, st as wouldReturnToSdkProvider, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as Synthesizer, tn as s3BucketDualStackDomainName, tr as redactSecretsForState, tt as secretSafeKeyDisplay, u as updatePartialMessage, ua as retryClassificationText, ui as resolveStateBucketWithDefaultAndSource, un as INTRINSIC_KEYS, ur as formatLockExpiry, ut as maskerOrIdentity, v as keptWholeReasonText, vi as uploadCfnTemplate, vn as malformedOutputsWarning, vr as isReadableBag, vt as slowCcOperationTimeoutMs, w as ccRoutedFinalSnapshotError, wi as derivePartitionAndUrlSuffix, wn as refuseDivergentRecordRegionForDestroy, wr as WorkGraph, wt as IntrinsicFunctionResolver, x as ATOMIC_FINAL_SNAPSHOT_TYPES, xi as expectedOwnerParam, xn as malformedResourcesWarning, xr as shouldRetainResource, xt as deleteSkipReason, y as mergeNoChangeOutputs, yi as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, yn as malformedRenderedContainersWarning, yr as orphansAfterRollback, yt as UNSPECIFIED_SKIP_REASON, z as isWarmThroughputDecrease, zi as DynamicReferenceRegionAmbiguousError, zn as TemplateParser, zr as buildDenyExternalAccessPolicy, zt as assertRegionMatch } from "./deploy-engine-CcFbq2q8.js";
2
+ import { C as displayIdent, S as UNRENDERABLE, T as truncateCodePoints, _ as withSkipPrefix, b as SECRET_REF_MAX_CODE_POINTS, 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, w as displaySafe, x as STACK_REF_MAX_CODE_POINTS } from "./logger-6LP1UiFA.js";
3
3
  import { n as awsClientDefaults } from "./aws-client-defaults-BoIxyCir.js";
4
- import { t as getCdkdVersion } from "./version-CWpPiHCn.js";
5
- import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-Dn1f0SJi.js";
4
+ import { t as getCdkdVersion } from "./version-B2-UuEte.js";
5
+ import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-DiZsq-4k.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";
@@ -17948,6 +17948,158 @@ function liveCeilingAlreadyMatches(live, requested) {
17948
17948
  return compared > 0;
17949
17949
  }
17950
17950
  /**
17951
+ * The sentinel DynamoDB accepts to CLEAR an on-demand request-unit maximum
17952
+ * (issue [#3373](https://github.com/go-to-k/cdkd/issues/3373)).
17953
+ *
17954
+ * LIVE-VERIFIED at BOTH positions this file sends from, which is what makes it
17955
+ * implementable at both at once rather than at one and not the other:
17956
+ *
17957
+ * - `UpdateTable.OnDemandThroughput` — AWS's own SDK model doc for the block,
17958
+ * and the measurement recorded on issue
17959
+ * [#1434](https://github.com/go-to-k/cdkd/issues/1434) (us-east-1 +
17960
+ * us-west-2, 2026-08-09), which probed the SINGLE-member shape this file
17961
+ * also sends: `{MaxWriteRequestUnits: -1}` against a live `{100, 100}` was
17962
+ * accepted and read back as `{MaxReadRequestUnits: 100}`;
17963
+ * - `UpdateTable.GlobalSecondaryIndexUpdates[].Update.OnDemandThroughput` —
17964
+ * the position issue #3373's body called UNMEASURED. It is not, and the
17965
+ * evidence is in TWO places, named separately because a reviewer checked
17966
+ * and one of them is not where an earlier draft of this said it was:
17967
+ * issue [#1423](https://github.com/go-to-k/cdkd/issues/1423)'s comment
17968
+ * thread carries the `{-1, -1}` transcript (us-east-1, 2026-08-09,
17969
+ * throwaway table) — ACCEPTED, with `DescribeTable` then reporting the
17970
+ * block ABSENT — and PR
17971
+ * [#1433](https://github.com/go-to-k/cdkd/pull/1433)'s body carries the
17972
+ * MIXED `keep 50 / reset -1` probe at the same position, re-run precisely
17973
+ * because only `{-1, -1}` had been verified. `dynamodb-globaltable-provider.ts`
17974
+ * has shipped on both since. #3373's dup-check dismissed those issues
17975
+ * because `AWS::DynamoDB::GlobalTable` spells its per-GSI ceilings
17976
+ * `Write` / `ReadOnDemandThroughputSettings` — true of the TEMPLATE and
17977
+ * irrelevant to the WIRE, where both providers send the same
17978
+ * `UpdateGlobalSecondaryIndexAction.OnDemandThroughput`.
17979
+ *
17980
+ * The read-back is ABSENCE, never `-1`, at both positions — so nothing on the
17981
+ * drift / `readCurrentState` side may expect this value.
17982
+ */
17983
+ const ON_DEMAND_LIMIT_RESET$1 = -1;
17984
+ /**
17985
+ * Which `OnDemandThroughput` members this update must explicitly REMOVE, as a
17986
+ * list of member names (issue #3373).
17987
+ *
17988
+ * An absent member KEEPS whatever maximum the table already carries, so a
17989
+ * template edit that drops a ceiling used to deploy GREEN, be recorded as
17990
+ * applied, and leave the live maximum in force forever — the #1160
17991
+ * absent-field-reset class, one level down inside a nested block.
17992
+ *
17993
+ * PER MEMBER, never per BLOCK: read and write maxima are independent template
17994
+ * values, so a whole-block branch would drop the single-member removal, which
17995
+ * is the likelier user edit (the `AWS::DynamoDB::GlobalTable` twin paid for
17996
+ * that once already).
17997
+ *
17998
+ * FOUR conditions, all necessary, each closing a different way this goes
17999
+ * wrong:
18000
+ *
18001
+ * 1. **the PREVIOUS side declared it AND it survived narrowing.**
18002
+ * `previousProperties` is a cdkd STATE record and the record is RAW — it
18003
+ * holds the spelling the TEMPLATE declared, including a member
18004
+ * {@link narrowOnDemandCeilings} REFUSED and cdkd therefore never sent. A
18005
+ * raw presence test would read such a member as "previously applied".
18006
+ * 2. **the DESIRED side does not declare it AT ALL, raw.** This is issue
18007
+ * [#1440](https://github.com/go-to-k/cdkd/issues/1440) exactly: a member
18008
+ * the grammar rejected is the template TRYING to SET a ceiling, and taking
18009
+ * the reset branch for it would silently CLEAR the ceiling being set. The
18010
+ * narrowed desired side cannot tell those apart; the raw one can.
18011
+ * 3. **AWS actually HOLDS the member right now.** It proves there is a maximum
18012
+ * to remove, and it makes the whole rule fail CLOSED when cdkd holds no
18013
+ * live snapshot — the correct direction for a send that DESTROYS a live
18014
+ * value.
18015
+ * 4. **this update leaves the table on PAY_PER_REQUEST** (`ceilingsSendable`).
18016
+ * Condition 3 does NOT subsume this, and believing it did was a BLOCKER on
18017
+ * PR go-to-k/cdkd#3401 (spec review, M0). The live snapshot is the ONE
18018
+ * `DescribeTable` at the top of `update()` and is never refreshed, while
18019
+ * both removal arms run AFTER the BillingMode flip — so on a template that
18020
+ * flips a live on-demand table to PROVISIONED **and** drops the ceiling,
18021
+ * the pre-flight refusals both decline (neither side DECLARES a ceiling),
18022
+ * the flip lands, and the stale snapshot then says the member is live: cdkd
18023
+ * would send `-1` to a now-PROVISIONED table, AWS would reject it, and a
18024
+ * previously-green no-op becomes a HALF-APPLIED deploy — the exact class
18025
+ * the #3392 pre-flight exists to prevent. `dynamodb-globaltable-provider.ts`
18026
+ * carries the same guard at both of its positions
18027
+ * (`onDemandCeilingLive` / `!billingFlipped`), named in PR
18028
+ * go-to-k/cdkd#1433's body as "skipped on a billing flip".
18029
+ *
18030
+ * What the skip COSTS was asked and MEASURED, not assumed (the
18031
+ * go-to-k/cdkd#3401 code review raised it; issue
18032
+ * [#1441](https://github.com/go-to-k/cdkd/issues/1441) had already
18033
+ * settled it). The worry is a round trip: deploy A sets a ceiling on a
18034
+ * PAY_PER_REQUEST table, deploy B flips to PROVISIONED and drops it (the
18035
+ * reset suppressed here), deploy C flips back declaring nothing — and
18036
+ * since deploy B's template is now the previous side, condition 1 can
18037
+ * never fire again, so a RETAINED ceiling would be unclearable. #1441's
18038
+ * probe (us-east-1, 2026-08-10, transcript on the issue) ran exactly that
18039
+ * sequence at the TABLE level: `DescribeTable` reports the block ABSENT
18040
+ * after the flip, and it does NOT come back on the flip home. Nothing is
18041
+ * stranded. The PER-INDEX round trip is not separately measured — issue
18042
+ * [#3403](https://github.com/go-to-k/cdkd/issues/3403) — but it fails in
18043
+ * the SAFE direction (a lingering maximum, never a destroyed one).
18044
+ *
18045
+ * Every member-presence test here is `Object.hasOwn`, not `in` (the
18046
+ * go-to-k/cdkd#3401 security review). `in` walks the prototype chain, and these
18047
+ * three tests gate a DESTRUCTIVE send off a cdkd STATE record — the own-key
18048
+ * convention `drift.ts`'s `hasOwnKey` / `ownValue` already follow for
18049
+ * state-derived bags (issue go-to-k/cdkd#2899). Not demonstrably reachable
18050
+ * today; the point is that the guard on a destroying path should not depend on
18051
+ * that staying true.
18052
+ *
18053
+ * `declared` present but NOT a plain block (a string, an array, an unresolved
18054
+ * intrinsic) yields NO removals: that value is forwarded VERBATIM for AWS to
18055
+ * reject by shape, and merging a sentinel into it is not expressible.
18056
+ *
18057
+ * And a declared block cdkd cannot READ AT ALL yields none either — a plain
18058
+ * object with no member surviving {@link narrowOnDemandCeilings}, i.e. only
18059
+ * rejected spellings or only names the grammar does not know (`{Ref: 'Unset'}`,
18060
+ * a mis-nested value). That is condition 2 one level up: such a block is not a
18061
+ * statement that the sibling member was REMOVED, it is a template cdkd could
18062
+ * not read, and clearing a live maximum on the strength of it is the same
18063
+ * destruction #1440 describes. An ABSENT desired side is the opposite and is
18064
+ * the headline case — nothing declared means nothing kept.
18065
+ */
18066
+ function onDemandCeilingRemovals(declared, previous, live, ceilingsSendable) {
18067
+ if (!ceilingsSendable) return [];
18068
+ if (live === void 0) return [];
18069
+ if (declared !== void 0 && !isPlainCapacityBlock(declared)) return [];
18070
+ const previousBlock = narrowOnDemandCeilings(previous).block;
18071
+ if (!isPlainCapacityBlock(previousBlock)) return [];
18072
+ const declaredBlock = isPlainCapacityBlock(declared) ? declared : void 0;
18073
+ if (declaredBlock !== void 0) {
18074
+ const declaredSurvivors = narrowOnDemandCeilings(declaredBlock).block;
18075
+ if (!isPlainCapacityBlock(declaredSurvivors) || !ON_DEMAND_CEILING_MEMBERS.some((member) => Object.hasOwn(declaredSurvivors, member))) return [];
18076
+ }
18077
+ const liveBag = live;
18078
+ const removals = [];
18079
+ for (const member of ON_DEMAND_CEILING_MEMBERS) {
18080
+ if (!Object.hasOwn(previousBlock, member)) continue;
18081
+ if (declaredBlock !== void 0 && Object.hasOwn(declaredBlock, member)) continue;
18082
+ if (toFiniteNumber(liveBag[member]) === void 0) continue;
18083
+ removals.push(member);
18084
+ }
18085
+ return removals;
18086
+ }
18087
+ /**
18088
+ * Merge the {@link onDemandCeilingRemovals} sentinels into a block bound for
18089
+ * the wire (issue #3373).
18090
+ *
18091
+ * A COPY, never a mutation of `block`: {@link narrowOnDemandCeilings} returns
18092
+ * its input BY IDENTITY when nothing needed rewriting, so mutating here would
18093
+ * write `-1` into the caller's own `properties` bag — which the engine then
18094
+ * records as state.
18095
+ */
18096
+ function withOnDemandCeilingRemovals(block, removals) {
18097
+ if (removals.length === 0) return block;
18098
+ const merged = { ...isPlainCapacityBlock(block) ? block : {} };
18099
+ for (const member of removals) merged[member] = ON_DEMAND_LIMIT_RESET$1;
18100
+ return merged;
18101
+ }
18102
+ /**
17951
18103
  * The capacity both TABLE-level forwarders substitute for a member the
17952
18104
  * template does not declare at all — `create()`'s `CreateTable` and the
17953
18105
  * BillingMode flip's `UpdateTable`. It is the pre-issue-#3147 `?? 5`, kept
@@ -18712,7 +18864,17 @@ var DynamoDBTableProvider = class {
18712
18864
  const billingOrThroughputChanged = billingMode !== prevBillingMode || JSON.stringify(properties["ProvisionedThroughput"]) !== JSON.stringify(previousProperties["ProvisionedThroughput"]);
18713
18865
  if (billingOrThroughputChanged && billingMode === "PROVISIONED" && (prevBillingMode === "PAY_PER_REQUEST" || liveBillingMode === "PAY_PER_REQUEST") && properties["OnDemandThroughput"] !== void 0) {
18714
18866
  const absentNote = properties["BillingMode"] === void 0 ? " (BillingMode is absent from the template, which CloudFormation resets to PROVISIONED)" : "";
18715
- throw new ProvisioningError(`AWS::DynamoDB::Table ${logicalId}: the template flips BillingMode to PROVISIONED${absentNote} while still declaring OnDemandThroughput, which AWS accepts only on a PAY_PER_REQUEST table. Nothing was applied. Remove OnDemandThroughput, or keep BillingMode: PAY_PER_REQUEST.`, logicalId, resourceType);
18867
+ throw new ProvisioningError(`AWS::DynamoDB::Table ${logicalId}: the template flips BillingMode to PROVISIONED${absentNote} while still declaring OnDemandThroughput, which AWS accepts only on a PAY_PER_REQUEST table. No BillingMode flip and no index change were applied. Remove OnDemandThroughput, or keep BillingMode: PAY_PER_REQUEST.`, resourceType, logicalId);
18868
+ }
18869
+ const effectiveBillingMode = billingOrThroughputChanged ? billingMode : liveBillingMode;
18870
+ if (effectiveBillingMode === "PROVISIONED") {
18871
+ const ceilingIndexNames = (Array.isArray(properties["GlobalSecondaryIndexes"]) ? properties["GlobalSecondaryIndexes"] : []).filter((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry)).filter((entry) => Boolean(entry["OnDemandThroughput"])).map((entry) => typeof entry["IndexName"] === "string" ? maskSecrets(entry["IndexName"]) : "<unnamed>");
18872
+ if (ceilingIndexNames.length > 0) {
18873
+ const modeNote = billingOrThroughputChanged ? "" : " (the table is already PROVISIONED in AWS and this update sends no BillingMode change)";
18874
+ const message = `AWS::DynamoDB::Table ${logicalId}: GlobalSecondaryIndexes ${ceilingIndexNames.join(", ")} declare OnDemandThroughput while the table's billing mode is PROVISIONED${modeNote}, which AWS accepts only on a PAY_PER_REQUEST table. Remove the per-index OnDemandThroughput, or set BillingMode: PAY_PER_REQUEST.`;
18875
+ if (context?.replayingState === true || context?.desiredFromAwsReadback === true) warn(`${message} Downgraded to a warning because this update's desired properties did not come from the template, so there is no template-side remedy; the per-index ceiling will be sent and AWS will reject it.`);
18876
+ else throw new ProvisioningError(`${message} No BillingMode flip and no index change were applied.`, resourceType, logicalId, physicalId);
18877
+ }
18716
18878
  }
18717
18879
  const liveIndexes = table?.GlobalSecondaryIndexes ?? [];
18718
18880
  const desiredIndexByName = /* @__PURE__ */ new Map();
@@ -18796,10 +18958,13 @@ var DynamoDBTableProvider = class {
18796
18958
  this.logger.debug(`Updated BillingMode/ProvisionedThroughput/TableClass on DynamoDB table ${physicalId}`);
18797
18959
  }
18798
18960
  if (JSON.stringify(properties["OnDemandThroughput"]) !== JSON.stringify(previousProperties["OnDemandThroughput"])) {
18799
- if (properties["OnDemandThroughput"]) {
18961
+ const ceilingRemovals = onDemandCeilingRemovals(properties["OnDemandThroughput"], previousProperties["OnDemandThroughput"], table?.OnDemandThroughput, effectiveBillingMode === "PAY_PER_REQUEST");
18962
+ if (properties["OnDemandThroughput"] || ceilingRemovals.length > 0) {
18963
+ const declaredForSend = properties["OnDemandThroughput"] ? this.coerceOnDemandCeilingsForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["OnDemandThroughput"], maskSecrets) : {};
18964
+ if (ceilingRemovals.length > 0) debug(`AWS::DynamoDB::Table ${logicalId}: removing the OnDemandThroughput ${ceilingRemovals.join(" / ")} the template no longer declares (sending ${ON_DEMAND_LIMIT_RESET$1}, DynamoDB's reset sentinel)`);
18800
18965
  await this.dynamoDBClient.send(new UpdateTableCommand({
18801
18966
  TableName: physicalId,
18802
- OnDemandThroughput: this.coerceOnDemandCeilingsForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["OnDemandThroughput"], maskSecrets)
18967
+ OnDemandThroughput: withOnDemandCeilingRemovals(declaredForSend, ceilingRemovals)
18803
18968
  }));
18804
18969
  await this.waitForTableActiveAfterUpdate(physicalId);
18805
18970
  this.logger.debug(`Updated OnDemandThroughput on DynamoDB table ${physicalId}`);
@@ -18868,7 +19033,7 @@ var DynamoDBTableProvider = class {
18868
19033
  this.logger.debug(`Disabled DynamoDB Stream on table ${physicalId}`);
18869
19034
  }
18870
19035
  }
18871
- if (JSON.stringify(properties["GlobalSecondaryIndexes"]) !== JSON.stringify(previousProperties["GlobalSecondaryIndexes"])) await this.applyGsiUpdates(physicalId, resourceType, logicalId, previousProperties["GlobalSecondaryIndexes"], properties["GlobalSecondaryIndexes"], properties["AttributeDefinitions"], gsiHandledByBillingFlip, currentLiveIndexNames, currentLiveIndexByName, liveCapacityComparable, maskSecrets);
19036
+ if (JSON.stringify(properties["GlobalSecondaryIndexes"]) !== JSON.stringify(previousProperties["GlobalSecondaryIndexes"])) await this.applyGsiUpdates(physicalId, resourceType, logicalId, previousProperties["GlobalSecondaryIndexes"], properties["GlobalSecondaryIndexes"], properties["AttributeDefinitions"], gsiHandledByBillingFlip, currentLiveIndexNames, currentLiveIndexByName, liveCapacityComparable, effectiveBillingMode === "PAY_PER_REQUEST", maskSecrets);
18872
19037
  if (JSON.stringify(properties["PointInTimeRecoverySpecification"]) !== JSON.stringify(previousProperties["PointInTimeRecoverySpecification"])) await this.applyPointInTimeRecovery(physicalId, properties["PointInTimeRecoverySpecification"], previousProperties["PointInTimeRecoverySpecification"], maskSecrets);
18873
19038
  if (JSON.stringify(properties["TimeToLiveSpecification"]) !== JSON.stringify(previousProperties["TimeToLiveSpecification"])) {
18874
19039
  this.assertNoActiveTtlAttributeNameChange(logicalId, resourceType, physicalId, properties["TimeToLiveSpecification"], previousProperties["TimeToLiveSpecification"]);
@@ -19062,7 +19227,7 @@ var DynamoDBTableProvider = class {
19062
19227
  * change", so a dropped block must be turned into an explicit
19063
19228
  * `PointInTimeRecoveryEnabled: false`).
19064
19229
  */
19065
- async applyPointInTimeRecovery(tableName, spec, previousSpec, maskSecrets = (text) => text) {
19230
+ async applyPointInTimeRecovery(tableName, spec, previousSpec, maskSecrets) {
19066
19231
  let enabled;
19067
19232
  let recoveryPeriodInDays;
19068
19233
  if (spec !== void 0 && spec !== null) {
@@ -19237,7 +19402,7 @@ var DynamoDBTableProvider = class {
19237
19402
  * pre-existing WarmThroughput "no clean remap" stance; precision changes flow
19238
19403
  * through on the create / first-enable path.
19239
19404
  */
19240
- async applyKinesisStreamingDestination(tableName, spec, previousSpec, maskSecrets = (text) => text) {
19405
+ async applyKinesisStreamingDestination(tableName, spec, previousSpec, maskSecrets) {
19241
19406
  const newArn = this.extractKinesisStreamArn(spec);
19242
19407
  const prevArn = this.extractKinesisStreamArn(previousSpec);
19243
19408
  if (newArn === prevArn) {
@@ -19427,7 +19592,7 @@ var DynamoDBTableProvider = class {
19427
19592
  * a remove-then-add pair, so this method throws on such a change rather than
19428
19593
  * silently dropping it.
19429
19594
  */
19430
- async applyGsiUpdates(physicalId, resourceType, logicalId, previousGsis, desiredGsis, desiredAttributeDefinitions, handledByBillingFlip = /* @__PURE__ */ new Set(), liveIndexNames, liveIndexByName, liveCapacityComparable = false, maskSecrets = (text) => text) {
19595
+ async applyGsiUpdates(physicalId, resourceType, logicalId, previousGsis, desiredGsis, desiredAttributeDefinitions, handledByBillingFlip, liveIndexNames, liveIndexByName, liveCapacityComparable, onDemandCeilingsSendable, maskSecrets) {
19431
19596
  const warn = (message) => this.logger.warn(maskSecrets(message));
19432
19597
  const debug = (message) => this.logger.debug(maskSecrets(message));
19433
19598
  const prev = previousGsis ?? [];
@@ -19459,7 +19624,7 @@ var DynamoDBTableProvider = class {
19459
19624
  adopted.WarmThroughput = adoptedWarm;
19460
19625
  adoptedHasMember = true;
19461
19626
  }
19462
- const adoptedCeiling = this.indexCeilingForSend(name, physicalId, gsi, void 0, live, maskSecrets);
19627
+ const adoptedCeiling = this.indexCeilingForSend(name, physicalId, gsi, void 0, live, onDemandCeilingsSendable, maskSecrets);
19463
19628
  if (adoptedCeiling) {
19464
19629
  adopted.OnDemandThroughput = adoptedCeiling;
19465
19630
  adoptedHasMember = true;
@@ -19495,7 +19660,7 @@ var DynamoDBTableProvider = class {
19495
19660
  update.WarmThroughput = warm;
19496
19661
  updateHasMember = true;
19497
19662
  }
19498
- const ceiling = this.indexCeilingForSend(name, physicalId, gsi, before, liveIndexByName?.get(name), maskSecrets);
19663
+ const ceiling = this.indexCeilingForSend(name, physicalId, gsi, before, liveIndexByName?.get(name), onDemandCeilingsSendable, maskSecrets);
19499
19664
  if (ceiling) {
19500
19665
  update.OnDemandThroughput = ceiling;
19501
19666
  updateHasMember = true;
@@ -19515,7 +19680,7 @@ var DynamoDBTableProvider = class {
19515
19680
  * two messages are disjoint by construction: this one fires only when
19516
19681
  * something DID resolve.
19517
19682
  */
19518
- coerceWarmThroughputForSend(scope, value, maskSecrets = (text) => text) {
19683
+ coerceWarmThroughputForSend(scope, value, maskSecrets) {
19519
19684
  const warn = (message) => this.logger.warn(maskSecrets(message));
19520
19685
  const coerced = coerceWarmThroughput(value);
19521
19686
  if (coerced === void 0) return void 0;
@@ -19618,24 +19783,23 @@ var DynamoDBTableProvider = class {
19618
19783
  * deploy succeed, so the engine records the un-sent ceiling as applied and
19619
19784
  * the deploy that FIXES the billing mode then compares equal and never
19620
19785
  * sends it — issue #3287's own defect, reintroduced by its own guard. The
19621
- * file's table-level twin states that reasoning at its own refusal. The
19622
- * correct shape is a PRE-FLIGHT refusal covering all THREE per-index send
19623
- * sites (the `Create` arm has carried the same defect since #3265), which
19624
- * is issue [#3392](https://github.com/go-to-k/cdkd/issues/3392). Until
19625
- * then AWS validates the constraint, exactly as it does for the
19626
- * TABLE-level arm and for the `Create` action.
19627
- *
19628
- * **That IS a behaviour change on this shape, stated rather than glossed**
19629
- * (round 3's C4). Before #3287 neither `Update` arm sent the member, so a
19630
- * PROVISIONED table declaring a per-index ceiling deployed GREEN with the
19631
- * ceiling silently ignored. Now the same-name arm sends it once the
19632
- * ceiling CHANGES, AWS rejects the `UpdateTable`, and the co-resident
19633
- * capacity / warm edits fail with it; the adopted-index arm has no
19634
- * recorded previous side to gate on, so it re-fails every deploy until the
19635
- * template is edited. Loud and recording nothing beats silent and lost,
19636
- * which is why it ships this way rather than behind the withdrawn skip —
19637
- * but it is a change, and #3392 is what makes it a cdkd-worded refusal
19638
- * before any call instead of an AWS rejection after one.
19786
+ * file's table-level twin states that reasoning at its own refusal.
19787
+ *
19788
+ * **The refusal it pointed at has SHIPPED** (issue
19789
+ * [#3392](https://github.com/go-to-k/cdkd/issues/3392)): `update()` walks
19790
+ * `GlobalSecondaryIndexes[]` before any call and throws when the EFFECTIVE
19791
+ * billing mode is PROVISIONED, which covers all THREE per-index send sites
19792
+ * at once (this arm, the same-name `Update` arm's caller, and the `Create`
19793
+ * action, which has carried the same defect since #3265) because none of
19794
+ * them is reached. So this arm's job is unchanged and stays deliberately
19795
+ * mode-blind: a gate here would be a SECOND spelling of one rule, and the
19796
+ * mid-update spelling is the one that was measured wrong.
19797
+ *
19798
+ * On the REPLAY path the pre-flight downgrades to a warning and this arm
19799
+ * still runs, so the remaining behaviour is what #3287's round 3 recorded:
19800
+ * the member goes out, AWS rejects the `UpdateTable`, and the co-resident
19801
+ * capacity / warm edits fail with it. Loud and recording nothing beats
19802
+ * silent and lost.
19639
19803
  * - **The change detector compares the NARROWED blocks WHEN NOTHING WAS
19640
19804
  * DROPPED, and the RAW ones otherwise** — the one place it differs from its
19641
19805
  * `ProvisionedThroughput` sibling two arms up. `'200'` and `200` are the
@@ -19681,21 +19845,39 @@ var DynamoDBTableProvider = class {
19681
19845
  * rejects by name (issue #3392) rather than a silently dropped change.
19682
19846
  * There is no `{0, 0}` analogue of the #1571 capacity trap.
19683
19847
  *
19684
- * What it deliberately does NOT do is REMOVE a ceiling. A desired side that
19685
- * declares none takes the truthiness gate and emits nothing, so the live
19686
- * maximum SURVIVES — `-1` is AWS's documented removal sentinel and an absent
19687
- * member keeps the maximum. That is byte-identical to what `update()`'s
19688
- * TABLE-level arm does with the same edit (its own comment states it), and
19689
- * implementing the reset on ONE of the two would make the same property
19690
- * behave differently at the table and at an index of the same table. The
19691
- * removal direction is filed for BOTH sites together as issue
19692
- * [#3373](https://github.com/go-to-k/cdkd/issues/3373), which also records
19693
- * why it is not an extrapolation from the GlobalTable measurement: `-1` is
19694
- * LIVE-VERIFIED at `UpdateTable.OnDemandThroughput` and UNMEASURED at this
19695
- * action's, and sending it DESTROYS a live ceiling.
19696
- */
19697
- indexCeilingForSend(name, physicalId, gsi, before, live, maskSecrets) {
19698
- if (!gsi.OnDemandThroughput) return void 0;
19848
+ * It DOES remove a ceiling, since issue
19849
+ * [#3373](https://github.com/go-to-k/cdkd/issues/3373) — and the removal is
19850
+ * the one thing here that runs AHEAD of the truthiness gate, because the
19851
+ * whole point is a desired side that declares nothing. An absent member KEEPS
19852
+ * whatever maximum the index already carries, so before #3373 a template edit
19853
+ * dropping a ceiling deployed GREEN, was recorded as applied, and left the
19854
+ * live maximum in force forever.
19855
+ *
19856
+ * {@link onDemandCeilingRemovals} owns every condition and
19857
+ * {@link ON_DEMAND_LIMIT_RESET} carries the measurement for THIS action's
19858
+ * position (issue #1423's probe transcript, not an extrapolation from the
19859
+ * table-level field's docs). Two properties of that rule matter most here:
19860
+ * the previous side is read through {@link narrowOnDemandCeilings} so a
19861
+ * member cdkd REFUSED cannot read as "previously applied"; the reset fires
19862
+ * only for a member AWS is OBSERVED to hold, which makes it fail CLOSED with
19863
+ * no live snapshot; and it is suppressed outright when this update FLIPS the
19864
+ * table to PROVISIONED (`onDemandCeilingsSendable`). Do not read the
19865
+ * observed-live condition as covering the flip — it does not, and believing
19866
+ * it did was a merge blocker: the live snapshot is taken before the flip and
19867
+ * never refreshed.
19868
+ *
19869
+ * The TABLE-level arm in `update()` takes the same rule in the same change,
19870
+ * because implementing it at ONE of the two positions would make the same
19871
+ * property behave differently at the table and at an index of the same table.
19872
+ */
19873
+ indexCeilingForSend(name, physicalId, gsi, before, live, onDemandCeilingsSendable, maskSecrets) {
19874
+ const removals = onDemandCeilingRemovals(gsi.OnDemandThroughput, before?.OnDemandThroughput, live?.OnDemandThroughput, onDemandCeilingsSendable);
19875
+ if (!gsi.OnDemandThroughput) {
19876
+ if (removals.length === 0) return void 0;
19877
+ const removalScope = this.indexScopeAt(name, physicalId, maskSecrets);
19878
+ this.logger.debug(maskSecrets(`${removalScope}: removing the OnDemandThroughput ${removals.join(" / ")} the template no longer declares (sending ${ON_DEMAND_LIMIT_RESET$1}, DynamoDB's reset sentinel)`));
19879
+ return withOnDemandCeilingRemovals({}, removals);
19880
+ }
19699
19881
  if (before !== void 0) {
19700
19882
  const desired = narrowOnDemandCeilings(gsi.OnDemandThroughput);
19701
19883
  const previous = narrowOnDemandCeilings(before.OnDemandThroughput);
@@ -19705,8 +19887,10 @@ var DynamoDBTableProvider = class {
19705
19887
  const scope = this.indexScopeAt(name, physicalId, maskSecrets);
19706
19888
  const warn = (message) => this.logger.warn(maskSecrets(message));
19707
19889
  const debug = (message) => this.logger.debug(maskSecrets(message));
19708
- const send = this.coerceOnDemandCeilingsForSend(scope, gsi.OnDemandThroughput, maskSecrets);
19709
- if (!isPlainCapacityBlock(send)) return send;
19890
+ const coerced = this.coerceOnDemandCeilingsForSend(scope, gsi.OnDemandThroughput, maskSecrets);
19891
+ if (!isPlainCapacityBlock(coerced)) return coerced;
19892
+ if (removals.length > 0) debug(`${scope}: removing the OnDemandThroughput ${removals.join(" / ")} the template no longer declares (sending ${ON_DEMAND_LIMIT_RESET$1}, DynamoDB's reset sentinel)`);
19893
+ const send = withOnDemandCeilingRemovals(coerced, removals);
19710
19894
  if (!ON_DEMAND_CEILING_MEMBERS.some((member) => member in send)) {
19711
19895
  if (narrowOnDemandCeilings(gsi.OnDemandThroughput).dropped.length > 0) debug(`${scope}: every declared OnDemandThroughput member was unusable, so no ceiling op was sent`);
19712
19896
  else warn(`${scope}: declares an OnDemandThroughput with no member DynamoDB accepts (${ON_DEMAND_CEILING_MEMBERS.join(" / ")}), so no ceiling op was sent. Check the member names.`);
@@ -19751,7 +19935,7 @@ var DynamoDBTableProvider = class {
19751
19935
  * value, which is pre-existing behaviour and costs one spread per declared
19752
19936
  * block.
19753
19937
  */
19754
- coerceIndexThroughputForCreate(logicalId, entry, maskSecrets = (text) => text) {
19938
+ coerceIndexThroughputForCreate(logicalId, entry, maskSecrets) {
19755
19939
  if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
19756
19940
  const bag = entry;
19757
19941
  const declaredWarm = bag["WarmThroughput"];
@@ -19837,7 +20021,7 @@ var DynamoDBTableProvider = class {
19837
20021
  * same fail-open reason — a misspelled `ReadCapacityUnit` reaches AWS and is
19838
20022
  * named there rather than vanishing here.
19839
20023
  */
19840
- coerceIndexCapacityBlock(scope, declared, maskSecrets = (text) => text) {
20024
+ coerceIndexCapacityBlock(scope, declared, maskSecrets) {
19841
20025
  if (!isPlainCapacityBlock(declared)) return void 0;
19842
20026
  const block = declared;
19843
20027
  let rewritten;
@@ -20045,7 +20229,7 @@ var DynamoDBTableProvider = class {
20045
20229
  * table that does not exist. {@link tableCapacityForSend}'s doc carries why
20046
20230
  * this type differs.
20047
20231
  */
20048
- warnUnusableProvisionedCapacity(scope, member, raw, maskSecrets = (text) => text) {
20232
+ warnUnusableProvisionedCapacity(scope, member, raw, maskSecrets) {
20049
20233
  const warn = (message) => this.logger.warn(maskSecrets(message));
20050
20234
  warn(`${scope}: ProvisionedThroughput.${member} ${JSON.stringify(maskLeafValue$1(raw, maskSecrets))} is not an Integer CloudFormation accepts (an optional sign and decimal digits only — no surrounding whitespace, hex, exponent or decimal point), so it was NOT sent and NO capacity was substituted for it. DynamoDB requires both members wherever a PROVISIONED ProvisionedThroughput block is declared — the table itself, and every GlobalSecondaryIndexes[] entry of a PROVISIONED table — and will reject the request naming this one; CloudFormation refuses the same template at properties validation. Check for an unresolved intrinsic, or spell the value as a decimal integer.`);
20051
20235
  }
@@ -20061,7 +20245,7 @@ var DynamoDBTableProvider = class {
20061
20245
  * Silent for an ABSENT or FALSY value — see {@link isRefusedWarmThroughput}
20062
20246
  * for why those two are not this warning's business.
20063
20247
  */
20064
- warnRefusedWarmThroughput(scope, value, maskSecrets = (text) => text) {
20248
+ warnRefusedWarmThroughput(scope, value, maskSecrets) {
20065
20249
  const warn = (message) => this.logger.warn(maskSecrets(message));
20066
20250
  if (!isRefusedWarmThroughput(value)) return;
20067
20251
  warn(`${scope}: the declared WarmThroughput ${JSON.stringify(maskLeafValue$1(value, maskSecrets))} carries no usable ReadUnitsPerSecond or WriteUnitsPerSecond, so it was NOT sent — DynamoDB accepts only those two members and rejects a request without either. Check for a misspelled member name or an unresolved intrinsic. Nothing else on this resource was affected.`);
@@ -20119,7 +20303,7 @@ var DynamoDBTableProvider = class {
20119
20303
  * `DescribeTable` reports for every index anyway, so there is no divergence
20120
20304
  * to hide.
20121
20305
  */
20122
- skipZeroCapacityIndexUpdate(indexName, physicalId, requested, maskSecrets = (text) => text) {
20306
+ skipZeroCapacityIndexUpdate(indexName, physicalId, requested, maskSecrets) {
20123
20307
  if (requested === void 0) return false;
20124
20308
  if (toFiniteNumber(requested.ReadCapacityUnits) !== 0 || toFiniteNumber(requested.WriteCapacityUnits) !== 0) return false;
20125
20309
  const warn = (message) => this.logger.warn(maskSecrets(message));
@@ -20152,7 +20336,7 @@ var DynamoDBTableProvider = class {
20152
20336
  * lower than current WarmThroughput, decreasing WarmThroughput is not
20153
20337
  * supported`.
20154
20338
  */
20155
- warmThroughputOpFor(indexName, physicalId, desired, previous, live, maskSecrets = (text) => text) {
20339
+ warmThroughputOpFor(indexName, physicalId, desired, previous, live, maskSecrets) {
20156
20340
  const warn = (message) => this.logger.warn(maskSecrets(message));
20157
20341
  const debug = (message) => this.logger.debug(maskSecrets(message));
20158
20342
  const scope = this.indexScopeAt(indexName, physicalId, maskSecrets);
@@ -20185,7 +20369,7 @@ var DynamoDBTableProvider = class {
20185
20369
  * of it — the wait is what makes the next op (there, the BillingMode flip)
20186
20370
  * legal.
20187
20371
  */
20188
- async runGsiOps(physicalId, ops, desiredAttributeDefinitions, maskSecrets = (text) => text) {
20372
+ async runGsiOps(physicalId, ops, desiredAttributeDefinitions, maskSecrets) {
20189
20373
  const debug = (message) => this.logger.debug(maskSecrets(message));
20190
20374
  for (const op of ops) {
20191
20375
  const input = {
@@ -20244,7 +20428,7 @@ var DynamoDBTableProvider = class {
20244
20428
  * resource clean. The message therefore says the warning may be the only
20245
20429
  * signal, and names both numbers so the template edit needs no second look.
20246
20430
  */
20247
- skipWarmThroughputDecrease(logicalId, physicalId, desired, live, maskSecrets = (text) => text) {
20431
+ skipWarmThroughputDecrease(logicalId, physicalId, desired, live, maskSecrets) {
20248
20432
  const warn = (message) => this.logger.warn(maskSecrets(message));
20249
20433
  if (!isWarmThroughputDecrease(desired, live)) return false;
20250
20434
  warn(`AWS::DynamoDB::Table ${logicalId}: the requested WarmThroughput ${JSON.stringify(maskDeep(desired, maskSecrets))} is lower than the ${JSON.stringify(maskDeep({
@@ -80176,11 +80360,12 @@ async function stateOrphanCommand(stackArgs, options) {
80176
80360
  if (!options.force) {
80177
80361
  for (const target of targets) if (await setup.lockManager.isLocked(stackName, target.region)) {
80178
80362
  const where = target.region ? displaySafe(target.region, { asciiOnly: true }) || "<unrenderable>" : "legacy";
80179
- throw new Error(`Stack '${displaySafe(stackName, { asciiOnly: true })}' (${where}) is locked. Run: ${buildForceUnlockCommand(stackName, target.region, {
80363
+ const recoveryCommand = buildForceUnlockCommand(stackName, target.region, {
80180
80364
  profile: options.profile,
80181
80365
  stateBucket: setup.bucket,
80182
80366
  statePrefix: options.statePrefix
80183
- }) || "a command cdkd cannot show safely: the recorded name or region would not survive a command line intact, so inspect the lock object directly"} first, or pass --force to remove anyway.`);
80367
+ });
80368
+ throw new Error(`Stack '${displaySafe(stackName, { asciiOnly: true })}' (${where}) is locked. ` + (recoveryCommand ? `Run: ${recoveryCommand} first, or pass --force to remove anyway.` : `${UNREPRODUCIBLE_LOCK_CLAUSE} Or pass --force to remove anyway.`));
80184
80369
  }
80185
80370
  }
80186
80371
  if (!options.yes && !options.force) {
@@ -88249,11 +88434,11 @@ async function pullEcrImage(imageUri, options) {
88249
88434
  const cached = ASSUMED_ROLE_CACHE.get(cacheKey);
88250
88435
  if (cached && isCredentialFresh(cached)) {
88251
88436
  assumed = cached;
88252
- logger.debug(`Reusing cached AssumeRole credentials for ${options.ecrRoleArn}`);
88437
+ logger.debug(`Reusing cached AssumeRole credentials for ${displayIdent(options.ecrRoleArn, { maxCodePoints: 612 })}`);
88253
88438
  } else {
88254
88439
  assumed = await assumeRoleForEcr(options.ecrRoleArn, callerRegion, logger);
88255
88440
  ASSUMED_ROLE_CACHE.set(cacheKey, assumed);
88256
- logger.info(`Assumed role ${options.ecrRoleArn} for ECR pull (account=${parsed.accountId}, region=${parsed.region})`);
88441
+ logger.info(`Assumed role ${displayIdent(options.ecrRoleArn, { maxCodePoints: 612 })} for ECR pull (account=${parsed.accountId}, region=${parsed.region})`);
88257
88442
  }
88258
88443
  } else if (crossAccount) logger.info(`Cross-account ECR pull: image account ${parsed.accountId} != caller ${callerAccount}. Using the caller's credentials; pass --ecr-role-arn <arn> if AWS rejects with AccessDenied.`);
88259
88444
  if (crossRegion) logger.info(`Cross-region ECR pull: image region ${parsed.region} != caller ${callerRegion ?? "(unset)"}. Authenticating against the image region directly.`);
@@ -88284,7 +88469,7 @@ async function pullEcrImage(imageUri, options) {
88284
88469
  * mirrors the convention used by `src/utils/role-arn.ts`.
88285
88470
  */
88286
88471
  async function assumeRoleForEcr(roleArn, callerRegion, logger) {
88287
- logger.debug(`Assuming role ${roleArn} for ECR pull...`);
88472
+ logger.debug(`Assuming role ${displayIdent(roleArn, { maxCodePoints: 612 })} for ECR pull...`);
88288
88473
  const sts = new STSClient({
88289
88474
  ...awsClientDefaults(),
88290
88475
  ...callerRegion && { region: callerRegion }
@@ -88295,7 +88480,7 @@ async function assumeRoleForEcr(roleArn, callerRegion, logger) {
88295
88480
  RoleSessionName: `cdkd-local-ecr-${Date.now()}`,
88296
88481
  DurationSeconds: 3600
88297
88482
  }))).Credentials;
88298
- if (!creds || !creds.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new LocalInvokeBuildError$1(`AssumeRole(${roleArn}) returned no usable credentials. Verify the role's trust policy allows your identity to assume it.`);
88483
+ if (!creds || !creds.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new LocalInvokeBuildError$1(`AssumeRole(${displayIdent(roleArn, { maxCodePoints: 612 })}) returned no usable credentials. Verify the role's trust policy allows your identity to assume it.`);
88299
88484
  return {
88300
88485
  accessKeyId: creds.AccessKeyId,
88301
88486
  secretAccessKey: creds.SecretAccessKey,
@@ -88305,7 +88490,7 @@ async function assumeRoleForEcr(roleArn, callerRegion, logger) {
88305
88490
  } catch (err) {
88306
88491
  if (err instanceof LocalInvokeBuildError$1) throw err;
88307
88492
  const reason = err instanceof Error ? err.message : String(err);
88308
- throw new LocalInvokeBuildError$1(`Failed to assume role ${roleArn} for ECR pull: ${reason}. Verify the role exists and its trust policy permits the caller's identity to assume it.`);
88493
+ throw new LocalInvokeBuildError$1(`Failed to assume role ${displayIdent(roleArn, { maxCodePoints: 612 })} for ECR pull: ${displaySafe(reason)}. Verify the role exists and its trust policy permits the caller's identity to assume it.`);
88309
88494
  } finally {
88310
88495
  sts.destroy();
88311
88496
  }
@@ -88623,7 +88808,7 @@ const CONTAINER_AWS_CREDENTIALS_PATH = "/cdkd-aws/credentials";
88623
88808
  */
88624
88809
  async function writeProfileCredentialsFile(profileName, creds) {
88625
88810
  if (profileName === "") throw new Error("writeProfileCredentialsFile: profile name must not be empty.");
88626
- if (/[\r\n[\]]/.test(profileName)) throw new Error(`writeProfileCredentialsFile: profile name '${profileName}' contains a forbidden character (any of CR, LF, '[', ']' would corrupt the INI file or the docker -e env var).`);
88811
+ if (/[\r\n[\]]/.test(profileName)) throw new Error(`writeProfileCredentialsFile: profile name ${displayIdent(profileName)} contains a forbidden character (any of CR, LF, '[', ']' would corrupt the INI file or the docker -e env var).`);
88627
88812
  const dir = await mkdtemp(path.join(tmpdir(), "cdkd-profile-creds-"));
88628
88813
  const hostPath = path.join(dir, "credentials");
88629
88814
  const lines = [
@@ -88632,7 +88817,15 @@ async function writeProfileCredentialsFile(profileName, creds) {
88632
88817
  `aws_secret_access_key = ${creds.secretAccessKey}`
88633
88818
  ];
88634
88819
  if (creds.sessionToken) lines.push(`aws_session_token = ${creds.sessionToken}`);
88635
- await writeFile(hostPath, lines.join("\n") + "\n", { mode: 384 });
88820
+ try {
88821
+ await writeFile(hostPath, lines.join("\n") + "\n", { mode: 384 });
88822
+ } catch (err) {
88823
+ await rm(dir, {
88824
+ recursive: true,
88825
+ force: true
88826
+ }).catch(() => {});
88827
+ throw err;
88828
+ }
88636
88829
  return {
88637
88830
  hostPath,
88638
88831
  containerPath: CONTAINER_AWS_CREDENTIALS_PATH,
@@ -90645,7 +90838,11 @@ async function resolveProfileCredentials(profile) {
90645
90838
  try {
90646
90839
  const credsProvider = sts.config.credentials;
90647
90840
  const creds = typeof credsProvider === "function" ? await credsProvider() : credsProvider;
90648
- if (!creds || !creds.accessKeyId || !creds.secretAccessKey) throw new Error(`--profile '${profile}': credential provider chain resolved without usable credentials. Check \`aws sso login --profile ` + profile + "` for SSO profiles, or `~/.aws/credentials` / `~/.aws/config` for regular profiles.");
90841
+ if (!creds || !creds.accessKeyId || !creds.secretAccessKey) {
90842
+ const shownProfile = displayIdent(profile);
90843
+ const ssoHint = isPasteableIdent(profile) ? "run `aws sso login --profile " + profile + "`" : "run `aws sso login` for that profile -- cdkd is not writing the command out, because it cannot vouch that the name as rendered is the one your shell would act on";
90844
+ throw new Error(`--profile ${shownProfile}: credential provider chain resolved without usable credentials. For SSO profiles, ${ssoHint}. For regular profiles, check \`~/.aws/credentials\` / \`~/.aws/config\`.`);
90845
+ }
90649
90846
  return {
90650
90847
  accessKeyId: creds.accessKeyId,
90651
90848
  secretAccessKey: creds.secretAccessKey,
@@ -90672,7 +90869,7 @@ async function assumeLambdaExecutionRole$1(roleArn, region) {
90672
90869
  RoleSessionName: `cdkd-local-start-api-${Date.now()}`,
90673
90870
  DurationSeconds: 3600
90674
90871
  }))).Credentials;
90675
- if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
90872
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${displayIdent(roleArn, { maxCodePoints: 612 })}) returned no usable credentials.`);
90676
90873
  return {
90677
90874
  accessKeyId: creds.AccessKeyId,
90678
90875
  secretAccessKey: creds.SecretAccessKey,
@@ -91234,7 +91431,7 @@ async function resolveEcsSecrets(entries, options = {}) {
91234
91431
  try {
91235
91432
  return await Promise.all(entries.map(async (entry) => {
91236
91433
  const value = await resolveOne(entry, secretsClient, ssmClient);
91237
- logger.debug(`Resolved secret ${entry.containerName}.${entry.name} (${entry.valueFrom})`);
91434
+ logger.debug(`Resolved secret ${displayIdent(entry.containerName)}.${displayIdent(entry.name)} (${displayIdent(entry.valueFrom, { maxCodePoints: SECRET_REF_MAX_CODE_POINTS })})`);
91238
91435
  return {
91239
91436
  ...entry,
91240
91437
  value
@@ -91251,7 +91448,7 @@ async function resolveOne(entry, secretsClient, ssmClient) {
91251
91448
  switch (shape.kind) {
91252
91449
  case "secrets-manager": return resolveSecretsManager(entry, shape, secretsClient);
91253
91450
  case "ssm": return resolveSsm(entry, shape, ssmClient);
91254
- case "unknown": throw new EcsSecretsResolutionError(`Container '${entry.containerName}' secret '${entry.name}' references an unsupported ValueFrom shape '${arn}'. Expected Secrets Manager ARN (optionally with :<json-key>::) or SSM Parameter ARN.`);
91451
+ case "unknown": throw new EcsSecretsResolutionError(`Container '${displayIdent(entry.containerName)}' secret '${displayIdent(entry.name)}' references an unsupported ValueFrom shape ${displayIdent(arn, { maxCodePoints: SECRET_REF_MAX_CODE_POINTS })}. Expected Secrets Manager ARN (optionally with :<json-key>::) or SSM Parameter ARN.`);
91255
91452
  }
91256
91453
  }
91257
91454
  /**
@@ -91284,20 +91481,20 @@ async function resolveSecretsManager(entry, shape, client) {
91284
91481
  try {
91285
91482
  secretString = (await client.send(new GetSecretValueCommand({ SecretId: shape.baseArn }))).SecretString;
91286
91483
  } catch (err) {
91287
- throw new EcsSecretsResolutionError(`Failed to resolve Secrets Manager secret for container '${entry.containerName}' / env '${entry.name}' (${shape.baseArn}): ${err instanceof Error ? err.message : String(err)}`);
91484
+ throw new EcsSecretsResolutionError(`Failed to resolve Secrets Manager secret for container '${displayIdent(entry.containerName)}' / env '${displayIdent(entry.name)}' (${displayIdent(shape.baseArn, { maxCodePoints: SECRET_REF_MAX_CODE_POINTS })}): ${displaySafe(err instanceof Error ? err.message : String(err))}`);
91288
91485
  }
91289
- if (secretString === void 0) throw new EcsSecretsResolutionError(`Secrets Manager returned no SecretString for container '${entry.containerName}' / env '${entry.name}' (${shape.baseArn}). Binary secrets are not supported.`);
91486
+ if (secretString === void 0) throw new EcsSecretsResolutionError(`Secrets Manager returned no SecretString for container '${displayIdent(entry.containerName)}' / env '${displayIdent(entry.name)}' (${displayIdent(shape.baseArn, { maxCodePoints: SECRET_REF_MAX_CODE_POINTS })}). Binary secrets are not supported.`);
91290
91487
  if (shape.jsonKey === void 0) return secretString;
91291
91488
  let parsed;
91292
91489
  try {
91293
91490
  parsed = JSON.parse(secretString);
91294
91491
  } catch (err) {
91295
91492
  const kind = err instanceof Error ? err.name : "unknown";
91296
- throw new EcsSecretsResolutionError(`Container '${entry.containerName}' secret '${entry.name}' specified json-key '${shape.jsonKey}' but the secret value is not valid JSON (${kind}). The parser detail is withheld because it would echo the secret plaintext.`);
91493
+ throw new EcsSecretsResolutionError(`Container '${displayIdent(entry.containerName)}' secret '${displayIdent(entry.name)}' specified json-key '${displaySafe(shape.jsonKey)}' but the secret value is not valid JSON (${displayIdent(kind)}). The parser detail is withheld because it would echo the secret plaintext.`);
91297
91494
  }
91298
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new EcsSecretsResolutionError(`Container '${entry.containerName}' secret '${entry.name}' specified json-key '${shape.jsonKey}' but the secret root is not a JSON object.`);
91495
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new EcsSecretsResolutionError(`Container '${displayIdent(entry.containerName)}' secret '${displayIdent(entry.name)}' specified json-key '${displaySafe(shape.jsonKey)}' but the secret root is not a JSON object.`);
91299
91496
  const value = parsed[shape.jsonKey];
91300
- if (value === void 0) throw new EcsSecretsResolutionError(`Container '${entry.containerName}' secret '${entry.name}' specified json-key '${shape.jsonKey}' but no such key exists in the secret JSON.`);
91497
+ if (value === void 0) throw new EcsSecretsResolutionError(`Container '${displayIdent(entry.containerName)}' secret '${displayIdent(entry.name)}' specified json-key '${displaySafe(shape.jsonKey)}' but no such key exists in the secret JSON.`);
91301
91498
  return typeof value === "string" ? value : JSON.stringify(value);
91302
91499
  }
91303
91500
  async function resolveSsm(entry, shape, client) {
@@ -91306,11 +91503,11 @@ async function resolveSsm(entry, shape, client) {
91306
91503
  Name: shape.name,
91307
91504
  WithDecryption: true
91308
91505
  }))).Parameter?.Value;
91309
- if (value === void 0) throw new EcsSecretsResolutionError(`SSM parameter '${shape.name}' returned no Value for container '${entry.containerName}' / env '${entry.name}'.`);
91506
+ if (value === void 0) throw new EcsSecretsResolutionError(`SSM parameter '${displayIdent(shape.name, { maxCodePoints: SECRET_REF_MAX_CODE_POINTS })}' returned no Value for container '${displayIdent(entry.containerName)}' / env '${displayIdent(entry.name)}'.`);
91310
91507
  return value;
91311
91508
  } catch (err) {
91312
91509
  if (err instanceof EcsSecretsResolutionError) throw err;
91313
- throw new EcsSecretsResolutionError(`Failed to resolve SSM parameter for container '${entry.containerName}' / env '${entry.name}' (${shape.name}): ${err instanceof Error ? err.message : String(err)}`);
91510
+ throw new EcsSecretsResolutionError(`Failed to resolve SSM parameter for container '${displayIdent(entry.containerName)}' / env '${displayIdent(entry.name)}' (${displayIdent(shape.name, { maxCodePoints: SECRET_REF_MAX_CODE_POINTS })}): ${displaySafe(err instanceof Error ? err.message : String(err))}`);
91314
91511
  }
91315
91512
  }
91316
91513
 
@@ -91914,8 +92111,9 @@ async function localRunTaskCommand(target, options) {
91914
92111
  const state = createEcsRunState();
91915
92112
  let sigintHandler;
91916
92113
  let sigintCount = 0;
92114
+ let detachedSuccessfully = false;
91917
92115
  let stateProvider;
91918
- let profileCredsFile;
92116
+ let channels;
91919
92117
  let cleanupPromise;
91920
92118
  const cleanup = async () => {
91921
92119
  if (!cleanupPromise) cleanupPromise = (async () => {
@@ -91924,10 +92122,10 @@ async function localRunTaskCommand(target, options) {
91924
92122
  } catch (err) {
91925
92123
  getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
91926
92124
  }
91927
- if (profileCredsFile) try {
91928
- await profileCredsFile.dispose();
92125
+ if (channels?.profileCredsFile) try {
92126
+ await channels.profileCredsFile.dispose();
91929
92127
  } catch (err) {
91930
- getLogger().debug(`Failed to remove profile credentials tmpdir ${profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
92128
+ getLogger().debug(`Failed to remove profile credentials tmpdir ${channels.profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
91931
92129
  }
91932
92130
  })();
91933
92131
  await cleanupPromise;
@@ -91993,33 +92191,20 @@ async function localRunTaskCommand(target, options) {
91993
92191
  resolvedRoleArn = options.assumeTaskRole;
91994
92192
  assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region);
91995
92193
  }
91996
- const sidecarCredentials = await resolveSidecarCredentials(options, assumedCredentials);
91997
- if (options.profile && sidecarCredentials && !assumedCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, sidecarCredentials);
92194
+ channels = await resolveTaskCredentialChannels(options, assumedCredentials);
91998
92195
  const envOverrides = readEnvOverridesFile$2(options.envVars);
91999
- const runOpts = {
92000
- cluster: options.cluster,
92001
- containerHost: options.containerHost,
92002
- skipPull: options.pull === false,
92003
- keepRunning: options.keepRunning,
92004
- detach: options.detach
92005
- };
92006
- if (envOverrides) runOpts.envOverrides = envOverrides;
92007
- if (sidecarCredentials) runOpts.taskCredentials = sidecarCredentials;
92008
- if (resolvedRoleArn) runOpts.taskRoleArn = resolvedRoleArn;
92009
- if (options.platform) runOpts.platformOverride = options.platform;
92010
- if (options.region) runOpts.region = options.region;
92011
- if (options.ecrRoleArn) runOpts.ecrRoleArn = options.ecrRoleArn;
92012
- if (profileCredsFile) runOpts.profileCredentialsFile = {
92013
- hostPath: profileCredsFile.hostPath,
92014
- containerPath: profileCredsFile.containerPath,
92015
- profileName: profileCredsFile.profileName
92016
- };
92196
+ const runOpts = buildRunEcsTaskOptions(options, channels, {
92197
+ ...envOverrides !== void 0 && { envOverrides },
92198
+ ...resolvedRoleArn !== void 0 && { resolvedRoleArn }
92199
+ });
92017
92200
  const hostGatewayExtraHosts = await resolveHostGatewayExtraHosts();
92018
92201
  if (hostGatewayExtraHosts.length > 0) runOpts.hostGatewayExtraHosts = hostGatewayExtraHosts;
92019
92202
  const result = await runEcsTask(task, runOpts, state);
92020
92203
  if (options.detach) {
92021
92204
  logger.info("Task containers started in detached mode; cdkd is exiting.");
92022
92205
  logger.info(`Use 'docker ps -a --filter network=${result.state.network?.networkName ?? "<network>"}' to inspect; tear down with 'docker rm -f' and 'docker network rm'.`);
92206
+ if (channels.profileCredsFile) logger.info(`The AWS credentials file mounted into the containers is NOT removed in detached mode: delete ${displayIdent(channels.profileCredsFile.hostPath)} once you tear them down.`);
92207
+ detachedSuccessfully = true;
92023
92208
  sigintCount = 99;
92024
92209
  return;
92025
92210
  }
@@ -92028,7 +92213,7 @@ async function localRunTaskCommand(target, options) {
92028
92213
  } finally {
92029
92214
  if (sigintHandler) process.off("SIGINT", sigintHandler);
92030
92215
  if (stateProvider) stateProvider.dispose();
92031
- if (!options.detach) await cleanup();
92216
+ if (!detachedSuccessfully) await cleanup();
92032
92217
  }
92033
92218
  }
92034
92219
  /**
@@ -92047,7 +92232,7 @@ async function resolvePlaceholderAccount(arn, region) {
92047
92232
  });
92048
92233
  try {
92049
92234
  const account = (await sts.send(new GetCallerIdentityCommand({}))).Account;
92050
- if (!account) throw new Error(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN '${arn}'. Pass the ARN explicitly: --assume-task-role <arn>`);
92235
+ if (!account) throw new Error(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN ${displayIdent(arn, { maxCodePoints: 612 })}. Pass the ARN explicitly: --assume-task-role <arn>`);
92051
92236
  return arn.split(TASK_ROLE_ACCOUNT_PLACEHOLDER).join(account);
92052
92237
  } finally {
92053
92238
  sts.destroy();
@@ -92069,7 +92254,7 @@ async function assumeTaskRole(roleArn, region) {
92069
92254
  RoleSessionName: `cdkd-local-run-task-${Date.now()}`,
92070
92255
  DurationSeconds: 3600
92071
92256
  }))).Credentials;
92072
- if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
92257
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${displayIdent(roleArn, { maxCodePoints: 612 })}) returned no usable credentials.`);
92073
92258
  return {
92074
92259
  accessKeyId: creds.AccessKeyId,
92075
92260
  secretAccessKey: creds.SecretAccessKey,
@@ -92256,6 +92441,106 @@ async function resolveSidecarCredentials(options, assumedCredentials) {
92256
92441
  if (assumedCredentials) return assumedCredentials;
92257
92442
  if (options.profile) return resolveProfileCredentials(options.profile);
92258
92443
  }
92444
+ /**
92445
+ * Resolve BOTH credential channels a locally-run ECS task receives, and decide
92446
+ * whether the second one exists at all.
92447
+ *
92448
+ * 1. `sidecarCredentials` — what the AWS-published
92449
+ * `amazon-ecs-local-container-endpoints` sidecar serves at
92450
+ * `169.254.170.2/role/<role>`; {@link resolveSidecarCredentials} owns the
92451
+ * precedence (issue #658).
92452
+ * 2. `profileCredsFile` — the ECS analogue of PR #670 (Lambda-container
92453
+ * fix-back finding #1). When `--profile <p>` is set AND `--assume-task-role`
92454
+ * did NOT produce credentials for this task, a host-side AWS shared
92455
+ * credentials file is written under `[<options.profile>]` and bind-mounted
92456
+ * read-only into every user container, so handler code calling
92457
+ * `fromIni({ profile: '<options.profile>' })` resolves to the same creds the
92458
+ * sidecar serves. Without it the SDK looks for `[<options.profile>]` in
92459
+ * `~/.aws/credentials` INSIDE the container and fails.
92460
+ *
92461
+ * Gating on `!assumedCredentials` preserves the documented precedence
92462
+ * (assume-task-role > profile-file > sidecar): when `--assume-task-role`
92463
+ * won, the sidecar's `/role/<arn>` endpoint already serves the assumed creds
92464
+ * and the file's env vars must NOT override them.
92465
+ *
92466
+ * ONE helper rather than two, and that is the substance of issue
92467
+ * go-to-k/cdkd#3378 rather than a tidying choice. Both of these lived inline in
92468
+ * `localRunTaskCommand`'s body, which meant the gate's annotation below was
92469
+ * fenced only for EXISTING (`local-surface-env-identity.test.ts` checks that an
92470
+ * annotation is there and carries a reason — it cannot check that the reason is
92471
+ * TRUE). And the reason is not a property of the `if` at all: it is a property
92472
+ * of the COMPOSITION, that `sidecarCredentials` on the `!assumedCredentials`
92473
+ * path can only have come from {@link resolveSidecarCredentials}'s `--profile`
92474
+ * arm. Extracting the three-line gate ALONE would have re-tested the same three
92475
+ * booleans and proved nothing; a test over this helper writes a real file and
92476
+ * can read the bytes back. `resolveSidecarCredentials`'s own doc already set
92477
+ * the precedent for the seam over a command-level harness — that harness would
92478
+ * have to mock the whole Synth + Docker + AWS pipeline, and a mock is exactly
92479
+ * what cannot answer "which identity's bytes landed in the file".
92480
+ *
92481
+ * cdkd-local-env-identity: the `!assumedCredentials` gate means
92482
+ * `resolveSidecarCredentials` reached its `--profile` arm, so this is
92483
+ * `resolveProfileCredentials` through
92484
+ * `awsClientDefaults({ ignoreAssumedRole: true })` — the caller's own chain,
92485
+ * never the `--role-arn` role — and never an `--assume-task-role` STS result,
92486
+ * which wins earlier and is served by the metadata sidecar instead.
92487
+ */
92488
+ async function resolveTaskCredentialChannels(options, assumedCredentials) {
92489
+ const sidecarCredentials = await resolveSidecarCredentials(options, assumedCredentials);
92490
+ if (options.profile && sidecarCredentials && !assumedCredentials) return {
92491
+ sidecarCredentials,
92492
+ profileCredsFile: await writeProfileCredentialsFile(options.profile, sidecarCredentials)
92493
+ };
92494
+ return {
92495
+ sidecarCredentials,
92496
+ profileCredsFile: void 0
92497
+ };
92498
+ }
92499
+ /**
92500
+ * Build the `RunEcsTaskOptions` bag from the options and the resolved credential
92501
+ * channels.
92502
+ *
92503
+ * Extracted for the WIRING, which is the half
92504
+ * {@link resolveTaskCredentialChannels} could not cover (go-to-k/cdkd#3390 round
92505
+ * 2, on go-to-k/cdkd#3394). That helper decides WHETHER a credentials file
92506
+ * exists and whose bytes are in it; this one decides whether the answer reaches
92507
+ * the runner at all. Between them sat a single assignment, and mutating it to
92508
+ * `undefined` left 114 tests green across five files: no file is bind-mounted,
92509
+ * so every handler calling `fromIni({ profile })` inside the task container
92510
+ * fails -- the exact defect PR go-to-k/cdkd#670 shipped this path to fix. "A
92511
+ * probed callee says nothing about its WIRING" is the rule, and this is the
92512
+ * seam that lets a unit test say something about it.
92513
+ *
92514
+ * PURE, and it takes the whole `channels` object rather than a pre-destructured
92515
+ * file so the test subject is the same value the resolver returns -- a helper
92516
+ * taking `profileCredsFile` alone would move the untested assignment rather
92517
+ * than remove it.
92518
+ *
92519
+ * The file is COPIED field by field rather than passed through: `dispose` stays
92520
+ * with the command, which owns the lifetime, and `RunEcsTaskOptions` deliberately
92521
+ * declares only the three fields the runner mounts with.
92522
+ */
92523
+ function buildRunEcsTaskOptions(options, channels, extra = {}) {
92524
+ const runOpts = {
92525
+ cluster: options.cluster,
92526
+ containerHost: options.containerHost,
92527
+ skipPull: options.pull === false,
92528
+ keepRunning: options.keepRunning,
92529
+ detach: options.detach
92530
+ };
92531
+ if (extra.envOverrides) runOpts.envOverrides = extra.envOverrides;
92532
+ if (channels.sidecarCredentials) runOpts.taskCredentials = channels.sidecarCredentials;
92533
+ if (extra.resolvedRoleArn) runOpts.taskRoleArn = extra.resolvedRoleArn;
92534
+ if (options.platform) runOpts.platformOverride = options.platform;
92535
+ if (options.region) runOpts.region = options.region;
92536
+ if (options.ecrRoleArn) runOpts.ecrRoleArn = options.ecrRoleArn;
92537
+ if (channels.profileCredsFile) runOpts.profileCredentialsFile = {
92538
+ hostPath: channels.profileCredsFile.hostPath,
92539
+ containerPath: channels.profileCredsFile.containerPath,
92540
+ profileName: channels.profileCredsFile.profileName
92541
+ };
92542
+ return runOpts;
92543
+ }
92259
92544
  function createLocalRunTaskCommand() {
92260
92545
  const cmd = new Command("run-task").description("Run an AWS::ECS::TaskDefinition locally — pulls/builds images, sets up a per-task docker network with the AWS-published metadata-endpoints sidecar, and starts every container in dependsOn order. Target accepts a CDK display path (MyStack/MyService/TaskDef) or stack-qualified logical ID (MyStack:MyServiceTaskDefXYZ1234). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed function role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries (#455). Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. A same-account pull in ANY region does not need this flag: the ECR client is built for the image URI's own region, so crossing a region costs nothing extra (issue #2536).")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--keep-running", "Don't docker rm -f the user containers on task exit (network + sidecar are still torn down). Use when you want to docker exec into a stopped container for post-mortems.").default(false)).addOption(new Option("--detach", "Start the containers in the background and exit (skip log streaming + auto teardown). Useful in CI smoke tests; caller manages container lifecycle.").default(false)).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt references to same-stack AWS::ECR::Repository resources with the deployed URI. Off by default — only the AWS pseudo-parameter tier (${AWS::AccountId} / ${AWS::Region}) is resolved without this flag.").default(false)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the cdkd stack name; pass an explicit value when the CFn stack name differs. Mutually exclusive with --from-state. Fn::GetAtt is warn-and-dropped in v1 (CFn ListStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-state when the same stack name has state in multiple regions, and with --from-cfn-stack as the CFn client region (cdkd does not have a separate --cfn-stack-region flag).").argParser(parseStackRegion)).action(withErrorHandling(localRunTaskCommand));
92261
92546
  [
@@ -93126,7 +93411,13 @@ async function resolveHostCredentialsForSigV4(options, resolved, loaded, region)
93126
93411
  if (assumeRoleArn) try {
93127
93412
  return await assumeAgentCoreExecutionRole(assumeRoleArn, region);
93128
93413
  } catch (err) {
93129
- logger.warn(`--assume-role: STS AssumeRole(${assumeRoleArn}) failed for --sigv4 signing: ${err instanceof Error ? err.message : String(err)}. Falling back to ${options.profile ? `--profile ${options.profile}` : "shell credentials"}.`);
93414
+ logger.warn(assumeRoleFallbackWarning({
93415
+ assumeRoleArn,
93416
+ purpose: "--sigv4 signing",
93417
+ error: err,
93418
+ profile: options.profile,
93419
+ fallbackWithoutProfile: "shell credentials"
93420
+ }));
93130
93421
  }
93131
93422
  if (options.profile) {
93132
93423
  const creds = await resolveProfileCredentials(options.profile);
@@ -93241,7 +93532,13 @@ async function resolveAgentCoreCodeImageFromS3(resolved, code, s3Source, options
93241
93532
  if (assumeRoleArn) try {
93242
93533
  credentials = await assumeAgentCoreExecutionRole(assumeRoleArn, region);
93243
93534
  } catch (err) {
93244
- logger.warn(`--assume-role: STS AssumeRole(${assumeRoleArn}) failed for the fromS3 bundle download: ${err instanceof Error ? err.message : String(err)}. Falling back to ${options.profile ? `--profile ${options.profile}` : "the default credentials"}.`);
93535
+ logger.warn(assumeRoleFallbackWarning({
93536
+ assumeRoleArn,
93537
+ purpose: "the fromS3 bundle download",
93538
+ error: err,
93539
+ profile: options.profile,
93540
+ fallbackWithoutProfile: "the default credentials"
93541
+ }));
93245
93542
  }
93246
93543
  const bundle = await downloadAndExtractS3Bundle(location, {
93247
93544
  ...region !== void 0 && { region },
@@ -93439,7 +93736,7 @@ async function applyAgentCoreCredentialEnv(dockerEnv, args) {
93439
93736
  if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
93440
93737
  assumeSucceeded = true;
93441
93738
  } catch (err) {
93442
- logger.warn(`--assume-role: STS AssumeRole(${args.assumeRoleArn}) failed: ${err instanceof Error ? err.message : String(err)}. Falling back to the developer's shell credentials.`);
93739
+ logger.warn(`--assume-role: STS AssumeRole(${displayIdent(args.assumeRoleArn, { maxCodePoints: 612 })}) failed: ${displaySafe(err instanceof Error ? err.message : String(err))}. Falling back to the developer's shell credentials.`);
93443
93740
  }
93444
93741
  }
93445
93742
  if (!assumeSucceeded) {
@@ -93452,6 +93749,53 @@ async function applyAgentCoreCredentialEnv(dockerEnv, args) {
93452
93749
  }
93453
93750
  }
93454
93751
  /**
93752
+ * The `--assume-role: STS AssumeRole(...) failed` warning, for the two sites
93753
+ * that emit the FALLBACK form of it — the `--sigv4` signing path and the fromS3 bundle download.
93754
+ *
93755
+ * ONE builder rather than two near-identical template literals, and that is
93756
+ * go-to-k/cdkd#3390's test review rather than tidiness. The two were twins, and
93757
+ * a twin is where a sanitizing rule goes half-applied: dropping `displayIdent`
93758
+ * from the fromS3 copy alone reddened only the source-shape fence, because the
93759
+ * `--sigv4` copy was the one with a behavioural case. As one PURE function both
93760
+ * sites inherit whatever this is tested to do, and there is one subject to test
93761
+ * rather than a second one somebody has to remember exists.
93762
+ *
93763
+ * TWO untrusted values, and both take `displayIdent` (issue
93764
+ * [#3377](https://github.com/go-to-k/cdkd/issues/3377) for the first, this PR's
93765
+ * security review for the second):
93766
+ *
93767
+ * - `profile` is user-supplied argv, and `ConsoleLogger.formatMessage`
93768
+ * sanitizes only a call's EXTRA ARGS, never the message itself — so the
93769
+ * message text is the last place it can be made safe. A MESSAGE, not a
93770
+ * command to paste, so `displayIdent` and not `isPasteableIdent`;
93771
+ * `resolveProfileCredentials` in `local-start-api.ts` is the site that needs
93772
+ * both spellings and shows the difference.
93773
+ * - `assumeRoleArn` is the WIDER boundary of the two, which is why it is here
93774
+ * at all: nothing validates it before `assumeAgentCoreExecutionRole` (whose
93775
+ * own throw lands in the very `catch` that calls this), and
93776
+ * `resolveAssumeRoleArn` can source it from a DEPLOYED STATE RECORD rather
93777
+ * than from argv. Every character of a legitimate ARN is in `PLAIN_IDENT`, so
93778
+ * it renders bare exactly as before. cdk-local's twin of this line flattens
93779
+ * the same value for the same reason; cdkd had nothing.
93780
+ *
93781
+ * `error` takes `displaySafe` — the DENYLIST, since an SDK error message is
93782
+ * free-form text rather than an identifier, which is the line
93783
+ * `display-safe.ts`'s header draws. The first cut stringified it and sanitized
93784
+ * NOTHING, and go-to-k/cdkd#3390's round-2 security review showed that was the
93785
+ * hole the other two guards leave open: STS answers an unparseable RoleArn with
93786
+ * a `ValidationError` that ECHOES THE SUBMITTED VALUE VERBATIM, so the
93787
+ * attacker-controlled ARN comes back through the error string and lands on the
93788
+ * same line the `displayIdent` two expressions to its left was added to
93789
+ * protect. A C1 byte, `U+0085`, `U+2028` and the bidi overrides all survive an
93790
+ * XML 1.0 response body. `formatError` sanitizes a `cause` for exactly this
93791
+ * reason and cannot help here, because this is a logger MESSAGE and
93792
+ * `ConsoleLogger.formatMessage` sanitizes only a call's EXTRA ARGS.
93793
+ */
93794
+ function assumeRoleFallbackWarning(args) {
93795
+ const fallback = args.profile ? `--profile ${displayIdent(args.profile)}` : args.fallbackWithoutProfile;
93796
+ return `--assume-role: STS AssumeRole(${displayIdent(args.assumeRoleArn, { maxCodePoints: 612 })}) failed for ${args.purpose}: ${displaySafe(args.error instanceof Error ? args.error.message : String(args.error))}. Falling back to ${fallback}.`;
93797
+ }
93798
+ /**
93455
93799
  * Resolve the role ARN to assume, honoring the three `--assume-role` forms.
93456
93800
  * Bare `--assume-role` uses the runtime's literal `RoleArn`; when that is an
93457
93801
  * intrinsic (the common L2 case — `Fn::GetAtt` to an auto-created role) it
@@ -93465,7 +93809,7 @@ function resolveAssumeRoleArn(options, resolved, loaded) {
93465
93809
  if (loaded) {
93466
93810
  const fromState = resolveExecutionRoleArnFromState(loaded, resolved.logicalId, "RoleArn");
93467
93811
  if (fromState) {
93468
- getLogger().debug(`--assume-role: resolved RoleArn from state: ${fromState}`);
93812
+ getLogger().debug(`--assume-role: resolved RoleArn from state: ${displayIdent(fromState)}`);
93469
93813
  return fromState;
93470
93814
  }
93471
93815
  }
@@ -93610,7 +93954,7 @@ async function assumeAgentCoreExecutionRole(roleArn, region) {
93610
93954
  RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-invoke-agentcore-${Date.now()}`,
93611
93955
  DurationSeconds: 3600
93612
93956
  }))).Credentials;
93613
- if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new CdkdError(`AssumeRole(${roleArn}) returned no usable credentials.`, "LOCAL_INVOKE_AGENTCORE_ASSUMEROLE_NO_CREDS");
93957
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new CdkdError(`AssumeRole(${displayIdent(roleArn, { maxCodePoints: 612 })}) returned no usable credentials.`, "LOCAL_INVOKE_AGENTCORE_ASSUMEROLE_NO_CREDS");
93614
93958
  return {
93615
93959
  accessKeyId: creds.AccessKeyId,
93616
93960
  secretAccessKey: creds.SecretAccessKey,
@@ -94223,7 +94567,7 @@ async function localInvokeCommand(target, options) {
94223
94567
  const arn = resolveExecutionRoleArnFromState(stateForRoleHint, lambda.logicalId);
94224
94568
  if (arn) {
94225
94569
  resolvedAssumeRoleArn = arn;
94226
- logger.info(`--assume-role: auto-resolved execution role from cdkd state: ${arn}`);
94570
+ logger.info(`--assume-role: auto-resolved execution role from cdkd state: ${displayIdent(arn, { maxCodePoints: 612 })}`);
94227
94571
  } else logger.warn(`--assume-role: could not resolve the execution role ARN from cdkd state for '${lambda.logicalId}'. Pass the ARN explicitly: --assume-role <arn>. Falling back to the developer's shell credentials.`);
94228
94572
  }
94229
94573
  } else maybeSuggestAssumeRole(options, stateForRoleHint, lambda.logicalId);
@@ -94691,7 +95035,7 @@ async function applyLambdaCredentialEnv(dockerEnv, args) {
94691
95035
  assumeSucceeded = true;
94692
95036
  } catch (err) {
94693
95037
  const reason = err instanceof Error ? err.message : String(err);
94694
- logger.warn(`--assume-role: STS AssumeRole(${args.assumeRoleArn}) failed: ${reason}. Falling back to the developer's shell credentials.`);
95038
+ logger.warn(`--assume-role: STS AssumeRole(${displayIdent(args.assumeRoleArn, { maxCodePoints: 612 })}) failed: ${displaySafe(reason)}. Falling back to the developer's shell credentials.`);
94695
95039
  }
94696
95040
  }
94697
95041
  if (!assumeSucceeded) {
@@ -94729,7 +95073,7 @@ async function assumeLambdaExecutionRole(roleArn, region) {
94729
95073
  RoleSessionName: `cdkd-local-invoke-${Date.now()}`,
94730
95074
  DurationSeconds: 3600
94731
95075
  }))).Credentials;
94732
- if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
95076
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${displayIdent(roleArn, { maxCodePoints: 612 })}) returned no usable credentials.`);
94733
95077
  return {
94734
95078
  accessKeyId: creds.AccessKeyId,
94735
95079
  secretAccessKey: creds.SecretAccessKey,
@@ -94850,7 +95194,7 @@ function materializeInlineCode(handler, source, fileExtension) {
94850
95194
  function suggestAssumeRoleFromState(state, logicalId) {
94851
95195
  const logger = getLogger();
94852
95196
  const roleArn = resolveExecutionRoleArnFromState(state, logicalId);
94853
- if (roleArn) logger.info(`Hint: the deployed function uses execution role ${roleArn}. Re-run with --assume-role to invoke under the deployed function's narrow permissions.`);
95197
+ if (roleArn) logger.info(`Hint: the deployed function uses execution role ${displayIdent(roleArn, { maxCodePoints: 612 })}. Re-run with --assume-role to invoke under the deployed function's narrow permissions.`);
94854
95198
  }
94855
95199
  /**
94856
95200
  * Emit the "re-run with `--assume-role`" hint, if it applies.
@@ -95010,4 +95354,4 @@ function buildProgram() {
95010
95354
 
95011
95355
  //#endregion
95012
95356
  export { buildProgram };
95013
- //# sourceMappingURL=program-dYPgGoOv.js.map
95357
+ //# sourceMappingURL=program-C1iVGcxJ.js.map