@go-to-k/cdkd 0.283.31 → 0.283.32
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-SQ0V0nFd.js → asg-provider-CVoisQ0L.js} +2 -2
- package/dist/{asg-provider-SQ0V0nFd.js.map → asg-provider-CVoisQ0L.js.map} +1 -1
- package/dist/cli.js +105 -25
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-AWFiXyhp.js → deploy-engine-rYTNzst0.js} +361 -25
- package/dist/deploy-engine-rYTNzst0.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-AWFiXyhp.js.map +0 -1
|
@@ -5560,6 +5560,27 @@ var AssetModeResolver = class {
|
|
|
5560
5560
|
function escapeRegExp$1(value) {
|
|
5561
5561
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5562
5562
|
}
|
|
5563
|
+
/**
|
|
5564
|
+
* Remove control characters from a value about to be PRINTED.
|
|
5565
|
+
*
|
|
5566
|
+
* Lives beside {@link escapeRegExp} for the reason that helper's own note
|
|
5567
|
+
* gives: this was spelled twice — in `src/cli/commands/diff-recursive.ts` and
|
|
5568
|
+
* in `src/deployment/outputs-export-alias.ts` — and copies of a
|
|
5569
|
+
* security-shaped helper drift. Both print the same class of string: a
|
|
5570
|
+
* resolved CloudFormation Output / `Export.Name`, which passed no CFn
|
|
5571
|
+
* validator, so it can carry ANSI escapes or bidi overrides straight into a
|
|
5572
|
+
* terminal or a CI log.
|
|
5573
|
+
*
|
|
5574
|
+
* C0 + DEL + C1 + the bidi marks and isolates. Apply on HUMAN-render paths
|
|
5575
|
+
* only: a `--json` payload is a machine interface where an export name is data
|
|
5576
|
+
* a consumer may match on, and mutating it there trades a correctness
|
|
5577
|
+
* regression for a display concern (see `stripDisplayOnlyChars`, which is the
|
|
5578
|
+
* narrower guard for an already-serialized payload and stays local to its
|
|
5579
|
+
* caller because its C0 exclusion is specific to that path).
|
|
5580
|
+
*/
|
|
5581
|
+
function stripControlChars(value) {
|
|
5582
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
|
|
5583
|
+
}
|
|
5563
5584
|
|
|
5564
5585
|
//#endregion
|
|
5565
5586
|
//#region src/assets/asset-redirect.ts
|
|
@@ -15076,7 +15097,7 @@ var CloudControlProvider = class {
|
|
|
15076
15097
|
if (context?.finalSnapshotIdentifier !== void 0) throw new ProvisioningError(`${logicalId} (${resourceType}) requires a final snapshot (DeletionPolicy: Snapshot), but the Cloud Control API delete route has no final-snapshot parameter. Re-run with --skip-final-snapshot after snapshotting manually, or retain the resource.`, resourceType, logicalId, physicalId);
|
|
15077
15098
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
15078
15099
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
15079
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
15100
|
+
const { ASGProvider } = await import("./asg-provider-CVoisQ0L.js").then((n) => n.n);
|
|
15080
15101
|
return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
|
|
15081
15102
|
}
|
|
15082
15103
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -20131,6 +20152,262 @@ function collectInlinePolicyNamesManagedBySiblings(targetPhysicalId, context, at
|
|
|
20131
20152
|
return result;
|
|
20132
20153
|
}
|
|
20133
20154
|
|
|
20155
|
+
//#endregion
|
|
20156
|
+
//#region src/deployment/outputs-export-alias.ts
|
|
20157
|
+
/**
|
|
20158
|
+
* Does CloudFormation suppress this output on this deploy?
|
|
20159
|
+
*
|
|
20160
|
+
* CFn does not create an output whose `Condition` evaluates false, and
|
|
20161
|
+
* `resolveOutputs` mirrors that (issue #1028). Unknown condition names are
|
|
20162
|
+
* KEPT, matching `filterResourcesByCondition` on the resource side — a
|
|
20163
|
+
* condition cdkd could not evaluate must not silently delete an output.
|
|
20164
|
+
*
|
|
20165
|
+
* DEPLOY-SIDE ONLY. `cdkd scrub` deliberately does not use this: see
|
|
20166
|
+
* {@link collectDeclaredOutputNames}.
|
|
20167
|
+
*/
|
|
20168
|
+
function isOutputSuppressedByCondition(output, conditions) {
|
|
20169
|
+
return output.Condition !== void 0 && conditions?.[output.Condition] === false;
|
|
20170
|
+
}
|
|
20171
|
+
/**
|
|
20172
|
+
* The output NAMES this deploy actually publishes — every declared output minus
|
|
20173
|
+
* the condition-suppressed ones.
|
|
20174
|
+
*
|
|
20175
|
+
* This is the set that owns keys in BOTH bags, and the reason the answer is
|
|
20176
|
+
* "published" rather than "declared": a suppressed output writes no value, so
|
|
20177
|
+
* it must not write a position source either, and its name is free for an
|
|
20178
|
+
* export alias to use. Reserving names for suppressed outputs would drop a
|
|
20179
|
+
* WORKING export the moment an unrelated condition went false.
|
|
20180
|
+
*
|
|
20181
|
+
* Sound at deploy time because these are the SAME condition values the deploy
|
|
20182
|
+
* itself acted on. Not sound in scrub — see {@link collectDeclaredOutputNames}.
|
|
20183
|
+
*/
|
|
20184
|
+
function collectPublishedOutputNames(outputs, conditions) {
|
|
20185
|
+
const names = /* @__PURE__ */ new Set();
|
|
20186
|
+
for (const [name, output] of Object.entries(outputs)) if (!isOutputSuppressedByCondition(output, conditions)) names.add(name);
|
|
20187
|
+
return names;
|
|
20188
|
+
}
|
|
20189
|
+
/**
|
|
20190
|
+
* Every DECLARED output name, conditions ignored — the set `cdkd scrub` tests
|
|
20191
|
+
* collisions against.
|
|
20192
|
+
*
|
|
20193
|
+
* Scrub must be a SUPERSET here, and the asymmetry with the deploy engine is
|
|
20194
|
+
* forced by what scrub can know. Its condition values are re-evaluated
|
|
20195
|
+
* best-effort, from template defaults only (the command takes no
|
|
20196
|
+
* `--parameters`), and `evaluateConditions` assumes FALSE on any evaluation
|
|
20197
|
+
* failure. So "suppressed" is both easy to hit spuriously and impossible to
|
|
20198
|
+
* confirm against the deploy that actually wrote the state.
|
|
20199
|
+
*
|
|
20200
|
+
* The two error directions are not symmetric, which is what settles the rule:
|
|
20201
|
+
*
|
|
20202
|
+
* - Judging a colliding output suppressed when the DEPLOY published it (the
|
|
20203
|
+
* spurious-false case above) makes scrub miss the collision, write the
|
|
20204
|
+
* exporting output's expression over the colliding key, and persist a
|
|
20205
|
+
* reference naming a DIFFERENT secret — the #1919 corruption, produced by
|
|
20206
|
+
* the remediation command itself.
|
|
20207
|
+
* - Judging it published when the deploy suppressed it costs one spurious
|
|
20208
|
+
* warning and one key redacted by VALUE match instead of by position. State
|
|
20209
|
+
* exactly what that costs, since an earlier revision understated it: the
|
|
20210
|
+
* value map is keyed by PLAINTEXT, so when two DISTINCT secrets resolve to
|
|
20211
|
+
* one value it keeps only the last, and that key can be persisted holding a
|
|
20212
|
+
* reference naming the OTHER secret. It is a smaller blast radius than the
|
|
20213
|
+
* first case (one key, and only when two secrets coincide, versus every
|
|
20214
|
+
* collision) but it is the same KIND of error, not a mere loss of precision.
|
|
20215
|
+
*
|
|
20216
|
+
* A wrong reference beats a lost precision bound, so scrub over-approximates.
|
|
20217
|
+
*/
|
|
20218
|
+
function collectDeclaredOutputNames(outputs) {
|
|
20219
|
+
return new Set(Object.keys(outputs));
|
|
20220
|
+
}
|
|
20221
|
+
/**
|
|
20222
|
+
* Would aliasing `exportName` land on a key another output owns?
|
|
20223
|
+
*
|
|
20224
|
+
* An output exporting under its OWN name is not a collision: the alias rewrites
|
|
20225
|
+
* the identical key with the identical value, and both bags then carry the same
|
|
20226
|
+
* source.
|
|
20227
|
+
*
|
|
20228
|
+
* Deliberately NOT extended to two outputs sharing one `Export.Name` with no
|
|
20229
|
+
* output of that name. Both bags stay consistent there (one iteration writes
|
|
20230
|
+
* both the value and its source), so it is not this issue's class — see
|
|
20231
|
+
* `docs/cross-stack-references.md`.
|
|
20232
|
+
*/
|
|
20233
|
+
function isExportAliasCollision(exportName, outputKey, ownedOutputNames) {
|
|
20234
|
+
return exportName !== outputKey && ownedOutputNames.has(exportName);
|
|
20235
|
+
}
|
|
20236
|
+
/**
|
|
20237
|
+
* Mirrors `secret-redaction`'s own `MIN_NEEDLE_LENGTH`. Duplicated rather than
|
|
20238
|
+
* imported because the two bounds answer different questions and should be free
|
|
20239
|
+
* to diverge: that one bounds what may be REWRITTEN, this one what may be
|
|
20240
|
+
* REFUSED or REPORTED.
|
|
20241
|
+
*/
|
|
20242
|
+
const MIN_SECRET_NEEDLE = 4;
|
|
20243
|
+
/**
|
|
20244
|
+
* Which recorded secrets are visible in `text`, or `undefined` when none is.
|
|
20245
|
+
*
|
|
20246
|
+
* ONE rule for both callers below, because they were inconsistent and the
|
|
20247
|
+
* inconsistency was a hole: the export-name check refused only an exact
|
|
20248
|
+
* whole-name match while the state-KEY scan did bounded containment over the
|
|
20249
|
+
* same kind of map — so `prod-<secret>-endpoint` was written as a state key by
|
|
20250
|
+
* the deploy and then reported by `cdkd scrub` as an UNREPAIRABLE leak. The tool
|
|
20251
|
+
* was creating the exact state it tells the user it cannot fix.
|
|
20252
|
+
*
|
|
20253
|
+
* A WHOLE-value match counts at any length; an EMBEDDED one only at or above
|
|
20254
|
+
* {@link MIN_SECRET_NEEDLE}. The bound is what makes containment safe to use for
|
|
20255
|
+
* a refusal at all — an unbounded scan over a degenerate one-character secret
|
|
20256
|
+
* matches almost every name and silently drops working exports — and its cost
|
|
20257
|
+
* is stated rather than hidden: a secret of three characters or fewer embedded
|
|
20258
|
+
* in a longer string is not seen. That value is indistinguishable from
|
|
20259
|
+
* coincidence, and the same bound already governs every substring redaction
|
|
20260
|
+
* cdkd performs.
|
|
20261
|
+
*
|
|
20262
|
+
* No empty-string case is needed at either caller: the resolver never records an
|
|
20263
|
+
* empty secret (`secret-redaction` says so — an empty needle would match every
|
|
20264
|
+
* leaf), and the length bound excludes it from the containment arm regardless.
|
|
20265
|
+
*/
|
|
20266
|
+
function secretsPresentIn(text, secrets) {
|
|
20267
|
+
if (!secrets || secrets.size === 0) return void 0;
|
|
20268
|
+
const exposure = /* @__PURE__ */ new Map();
|
|
20269
|
+
for (const [plaintext, expression] of secrets) if (text === plaintext || plaintext.length >= MIN_SECRET_NEEDLE && text.includes(plaintext)) exposure.set(plaintext, expression);
|
|
20270
|
+
return exposure.size > 0 ? exposure : void 0;
|
|
20271
|
+
}
|
|
20272
|
+
/**
|
|
20273
|
+
* The secrets present in a resolved `Export.Name`, or `undefined` when it
|
|
20274
|
+
* carries none.
|
|
20275
|
+
*
|
|
20276
|
+
* An `Export.Name` may be an intrinsic (`Fn::Sub` / `Fn::Join`), and those
|
|
20277
|
+
* substitute dynamic references — so the resolved name can contain a resolved
|
|
20278
|
+
* secret. That name would become a state KEY, and every redaction pass walks
|
|
20279
|
+
* VALUES only, so the plaintext would land in `state.json` and be republished
|
|
20280
|
+
* into the exports index.
|
|
20281
|
+
*
|
|
20282
|
+
* TWO signals, and neither subsumes the other:
|
|
20283
|
+
*
|
|
20284
|
+
* - `substitutedIntoName` — the caller resolves the name with its OWN
|
|
20285
|
+
* `recordedSecretValues` map, so a non-empty map means the resolver
|
|
20286
|
+
* substituted a secret INTO THIS NAME. Exact, and the only arm that can see a
|
|
20287
|
+
* substituted secret SHORTER than the containment bound below.
|
|
20288
|
+
* - a bounded containment scan of `recordedThisPass` ({@link secretsPresentIn}),
|
|
20289
|
+
* which catches plaintext that arrived by any other route — a literal name, a
|
|
20290
|
+
* cache hit, an `Fn::Sub` variable echoing the value.
|
|
20291
|
+
*
|
|
20292
|
+
* An earlier revision had only the first, calling the scan unpromising because
|
|
20293
|
+
* an UNBOUNDED one is: a degenerate one-character recorded secret would make
|
|
20294
|
+
* every name containing that character "secret" and silently drop working
|
|
20295
|
+
* exports. The bound is what makes the scan safe, and without the scan the
|
|
20296
|
+
* deploy wrote `prod-<secret>-endpoint` as a state key that `cdkd scrub` then
|
|
20297
|
+
* reported as unrepairable.
|
|
20298
|
+
*
|
|
20299
|
+
* The containment arm applies with no order caveat any more: the deploy engine
|
|
20300
|
+
* resolves EVERY output value
|
|
20301
|
+
* before deciding any alias, so this arm sees the complete map whatever the
|
|
20302
|
+
* declaration order. (An earlier revision of this paragraph said catching that
|
|
20303
|
+
* required "resolving the whole template before deciding anything" and called
|
|
20304
|
+
* it impractical — the value pass already does exactly that, at no extra cost.
|
|
20305
|
+
* The residual is now only a secret first substituted by ANOTHER output's
|
|
20306
|
+
* `Export.Name`, since those resolve in the second pass, in declaration order.)
|
|
20307
|
+
*
|
|
20308
|
+
* No empty-string special case, in either map: the resolver never records an
|
|
20309
|
+
* empty secret (`secret-redaction.ts` states it — an empty needle would match
|
|
20310
|
+
* every leaf), so an `''` key cannot reach here, and a guard against it would
|
|
20311
|
+
* be a branch no test could ever distinguish.
|
|
20312
|
+
*/
|
|
20313
|
+
function exportNameSecretExposure(exportName, substitutedIntoName, recordedThisPass) {
|
|
20314
|
+
const exposure = new Map(substitutedIntoName);
|
|
20315
|
+
for (const [plaintext, expression] of secretsPresentIn(exportName, recordedThisPass) ?? []) exposure.set(plaintext, expression);
|
|
20316
|
+
return exposure.size > 0 ? exposure : void 0;
|
|
20317
|
+
}
|
|
20318
|
+
/**
|
|
20319
|
+
* Secrets visible in a state KEY, or `undefined` when there are none.
|
|
20320
|
+
*
|
|
20321
|
+
* Unlike {@link exportNameSecretExposure} there is no resolution to attribute
|
|
20322
|
+
* this to: the key was written by an EARLIER binary, so containment is the only
|
|
20323
|
+
* available signal. It is therefore bounded the same way `secret-redaction`
|
|
20324
|
+
* bounds its own substring scan — a value shorter than {@link MIN_SECRET_NEEDLE}
|
|
20325
|
+
* is matched only as the WHOLE key — because an unbounded containment scan over
|
|
20326
|
+
* a degenerate short secret flags every key in the state and fails the
|
|
20327
|
+
* `--dry-run --fail` CI gate repo-wide, which is the availability failure the
|
|
20328
|
+
* export-name check was redesigned to avoid.
|
|
20329
|
+
*
|
|
20330
|
+
* Bound and cost are {@link secretsPresentIn}'s; see there.
|
|
20331
|
+
*/
|
|
20332
|
+
function stateKeySecretExposure(key, secrets) {
|
|
20333
|
+
return secretsPresentIn(key, secrets);
|
|
20334
|
+
}
|
|
20335
|
+
/**
|
|
20336
|
+
* Replace EVERY occurrence of every exposed secret with the mask.
|
|
20337
|
+
*
|
|
20338
|
+
* Deliberately not `maskSecretsInText`: that helper skips needles shorter than
|
|
20339
|
+
* its minimum length, which is right when scanning arbitrary bags for
|
|
20340
|
+
* coincidental matches but wrong here, where the values are known to be IN this
|
|
20341
|
+
* string. Feeding a detected-but-unmaskable name to it printed the secret under
|
|
20342
|
+
* a "masked:" label — a message asserting a protection it had not performed.
|
|
20343
|
+
* Longest-first so a secret containing another is masked whole.
|
|
20344
|
+
*/
|
|
20345
|
+
function maskEveryOccurrence(text, exposure) {
|
|
20346
|
+
let out = text;
|
|
20347
|
+
for (const value of Array.from(exposure.keys()).sort((a, b) => b.length - a.length)) out = out.split(value).join("***");
|
|
20348
|
+
return out;
|
|
20349
|
+
}
|
|
20350
|
+
/**
|
|
20351
|
+
* Warning for an `Export.Name` that resolved to something containing secret
|
|
20352
|
+
* plaintext. Refused rather than published: the name would be a state KEY, and
|
|
20353
|
+
* keys are never redacted.
|
|
20354
|
+
*
|
|
20355
|
+
* The name is shown MASKED, and omitted entirely if masking somehow left it
|
|
20356
|
+
* unchanged. stderr is a reader like any other, so the invariant is absolute: a
|
|
20357
|
+
* message must never claim a masking it did not perform.
|
|
20358
|
+
*/
|
|
20359
|
+
function secretBearingExportNameWarning(outputKey, exportName, exposure) {
|
|
20360
|
+
const masked = stripControlChars(maskEveryOccurrence(exportName, exposure));
|
|
20361
|
+
const shown = masked === stripControlChars(exportName) ? "" : `(masked: "${masked}") `;
|
|
20362
|
+
return `Output ${stripControlChars(outputKey)} has an Export.Name that resolves to a value containing a secret ${shown}— skipping the export alias. An export name becomes a key in state.json and in the exports index, and redaction rewrites VALUES only, so publishing it would persist the secret in plaintext. Use a non-secret Export.Name.`;
|
|
20363
|
+
}
|
|
20364
|
+
/**
|
|
20365
|
+
* Warning for a state KEY that already holds secret plaintext — the residue an
|
|
20366
|
+
* EARLIER binary left when it published an export name that resolved to one.
|
|
20367
|
+
*
|
|
20368
|
+
* `cdkd scrub` cannot repair this. Every redaction pass rewrites VALUES; a key
|
|
20369
|
+
* is the export's identity, so renaming it here would silently retire an export
|
|
20370
|
+
* consumers resolve by name, and dropping it would delete a live export. The
|
|
20371
|
+
* remedy is in the template: give the output a non-secret `Export.Name` and
|
|
20372
|
+
* redeploy, which rewrites `state.outputs` wholesale and republishes the index.
|
|
20373
|
+
* Reported so the `--dry-run --fail` CI gate stops calling such a state clean.
|
|
20374
|
+
*/
|
|
20375
|
+
function secretBearingStateKeyWarning(stackName, key, exposure) {
|
|
20376
|
+
return `State for ${stripControlChars(stackName)} holds an output KEY containing a secret (masked: "${stripControlChars(maskEveryOccurrence(key, exposure))}") — cdkd scrub cannot rewrite a key, only a value, because the key IS the export name consumers resolve by. Give that output a non-secret Export.Name and redeploy: the next deploy replaces state.outputs and the exports index entirely. ROTATE the exposed secret.`;
|
|
20377
|
+
}
|
|
20378
|
+
/**
|
|
20379
|
+
* Warning for an `Export.Name` colliding with an output NAME it does not own.
|
|
20380
|
+
*
|
|
20381
|
+
* Names both outputs because the two are equally likely to be the mistake, and
|
|
20382
|
+
* says which value survives — the export is skipped, so the key keeps the
|
|
20383
|
+
* output's own value.
|
|
20384
|
+
*
|
|
20385
|
+
* No masking here, and none is needed: the name printed is one that MATCHED a
|
|
20386
|
+
* declared output name, i.e. template text, and a name carrying a secret is
|
|
20387
|
+
* refused by {@link secretBearingExportNameWarning} before this is reached.
|
|
20388
|
+
*/
|
|
20389
|
+
function exportAliasCollisionWarning(outputKey, exportName) {
|
|
20390
|
+
const shown = stripControlChars(exportName);
|
|
20391
|
+
const from = stripControlChars(outputKey);
|
|
20392
|
+
return `Output ${from} exports as "${shown}", which is also the name of another output in this stack — skipping the export alias, so output ${shown} keeps its own value and the export is not published. A consumer's Fn::ImportValue on "${shown}" therefore resolves to output ${shown}, NOT to ${from} (CloudFormation would publish both). Rename the export, or the colliding output.`;
|
|
20393
|
+
}
|
|
20394
|
+
/**
|
|
20395
|
+
* Warning for the same collision seen by `cdkd scrub`, whose remedy differs.
|
|
20396
|
+
*
|
|
20397
|
+
* Scrub does not resolve outputs — it redacts state written by an EARLIER
|
|
20398
|
+
* binary, where the alias may have won the colliding key — so it cannot claim
|
|
20399
|
+
* the key belongs to either output. It drops the position source for that key
|
|
20400
|
+
* and lets the value scan decide from the plaintext actually stored, which is
|
|
20401
|
+
* why this message promises something weaker than the deploy-time one. It can
|
|
20402
|
+
* also fire on a template the deploy handled cleanly, per
|
|
20403
|
+
* {@link collectDeclaredOutputNames}.
|
|
20404
|
+
*/
|
|
20405
|
+
function exportAliasCollisionScrubWarning(outputKey, exportName, secrets) {
|
|
20406
|
+
const exposure = secretsPresentIn(exportName, secrets);
|
|
20407
|
+
const shown = stripControlChars(exposure ? maskEveryOccurrence(exportName, exposure) : exportName);
|
|
20408
|
+
return `Output ${stripControlChars(outputKey)} exports as "${shown}", which is also the name of another output in this stack — state cannot say which of the two the stored value under "${shown}" came from, so that key is redacted by value match instead of by template position, and two references resolving to the same value could still collapse there. Rename the export, or the colliding output, and redeploy.`;
|
|
20409
|
+
}
|
|
20410
|
+
|
|
20134
20411
|
//#endregion
|
|
20135
20412
|
//#region src/utils/colors.ts
|
|
20136
20413
|
/**
|
|
@@ -22294,7 +22571,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
22294
22571
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
22295
22572
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
22296
22573
|
function getCdkdVersion() {
|
|
22297
|
-
return "0.283.
|
|
22574
|
+
return "0.283.32";
|
|
22298
22575
|
}
|
|
22299
22576
|
/**
|
|
22300
22577
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -22935,6 +23212,14 @@ var DeployEngine = class {
|
|
|
22935
23212
|
*/
|
|
22936
23213
|
outputsTemplateSource = {};
|
|
22937
23214
|
/**
|
|
23215
|
+
* Whether {@link outputsTemplateSource} may be used to POSITION the outputs
|
|
23216
|
+
* redaction. False once an outputs pass threw partway: the post-loop
|
|
23217
|
+
* name pass never ran, so the bag holds only the alias keys written before
|
|
23218
|
+
* the throw — a partial source built from THIS template, while the bag the
|
|
23219
|
+
* failure path then redacts is the PREVIOUS deploy's. Reset per `deploy()`.
|
|
23220
|
+
*/
|
|
23221
|
+
outputsSourceUsable = true;
|
|
23222
|
+
/**
|
|
22938
23223
|
* Per-logical-id snapshot of the intrinsic-RESOLVED desired properties
|
|
22939
23224
|
* each CREATE / UPDATE attempted (issue #1198). Written just before the
|
|
22940
23225
|
* provider call; read only when the op FAILS, to journal the failed op's
|
|
@@ -22979,6 +23264,7 @@ var DeployEngine = class {
|
|
|
22979
23264
|
this.perResourceTemplateProps = /* @__PURE__ */ new Map();
|
|
22980
23265
|
this.outputSecrets = /* @__PURE__ */ new Map();
|
|
22981
23266
|
this.outputsTemplateSource = {};
|
|
23267
|
+
this.outputsSourceUsable = true;
|
|
22982
23268
|
this.resolver.resetPhysicalIdFallbackCount();
|
|
22983
23269
|
return withStackName(stackName, () => this.doDeploy(stackName, template));
|
|
22984
23270
|
}
|
|
@@ -23022,7 +23308,7 @@ var DeployEngine = class {
|
|
|
23022
23308
|
*/
|
|
23023
23309
|
redactOutputs(outputs) {
|
|
23024
23310
|
if (this.outputSecrets.size === 0) return outputs;
|
|
23025
|
-
return redactSecretsForState(outputs, this.outputSecrets, this.outputsTemplateSource);
|
|
23311
|
+
return redactSecretsForState(outputs, this.outputSecrets, this.outputsSourceUsable ? this.outputsTemplateSource : void 0);
|
|
23026
23312
|
}
|
|
23027
23313
|
/**
|
|
23028
23314
|
* Redact resolved secret plaintext out of rollback-journal operations (GHSA
|
|
@@ -24626,9 +24912,30 @@ var DeployEngine = class {
|
|
|
24626
24912
|
});
|
|
24627
24913
|
}
|
|
24628
24914
|
/**
|
|
24629
|
-
*
|
|
24915
|
+
* What a failed Output resolution does, shared by both passes of
|
|
24916
|
+
* {@link resolveOutputs} so they cannot drift — the alias pass reports the
|
|
24917
|
+
* SAME failure for a name it could not resolve as the value pass does for a
|
|
24918
|
+
* value, which is what the single-pass shape did when both lived in one
|
|
24919
|
+
* `try`.
|
|
24630
24920
|
*
|
|
24631
|
-
*
|
|
24921
|
+
* Issue #1111 item 2: under `--strict-getatt` an unresolvable Output fails
|
|
24922
|
+
* the deploy instead of silently publishing nothing (which breaks downstream
|
|
24923
|
+
* `Fn::ImportValue` consumers with "export not found" long after this deploy
|
|
24924
|
+
* exits 0).
|
|
24925
|
+
*/
|
|
24926
|
+
handleOutputResolutionFailure(error, outputKey, outputs) {
|
|
24927
|
+
if (this.options.strictGetAtt) throw new Error(`Failed to resolve output ${outputKey}: ${error instanceof Error ? error.message : String(error)} (--strict-getatt promotes output resolution failures to deploy errors; drop the flag to warn and skip the output instead)`, { cause: error });
|
|
24928
|
+
this.logger.warn(`Failed to resolve output ${outputKey}: ${String(error)}`);
|
|
24929
|
+
outputs[outputKey] = void 0;
|
|
24930
|
+
}
|
|
24931
|
+
/**
|
|
24932
|
+
* Resolve stack outputs from template and resource attributes.
|
|
24933
|
+
*
|
|
24934
|
+
* Uses `IntrinsicFunctionResolver` for full CloudFormation intrinsic function
|
|
24935
|
+
* support, and runs in TWO passes — every value, then every export alias —
|
|
24936
|
+
* so an alias decision sees the complete set of secrets this pass resolved
|
|
24937
|
+
* rather than whatever the declaration order happened to have recorded by
|
|
24938
|
+
* then (issue #1919).
|
|
24632
24939
|
*/
|
|
24633
24940
|
async resolveOutputs(template, resources, stackName, parameterValues, conditions) {
|
|
24634
24941
|
if (!template.Outputs) return {};
|
|
@@ -24639,29 +24946,58 @@ var DeployEngine = class {
|
|
|
24639
24946
|
...parameterValues && { parameters: parameterValues },
|
|
24640
24947
|
...conditions && { conditions }
|
|
24641
24948
|
}, stackName);
|
|
24642
|
-
|
|
24643
|
-
|
|
24644
|
-
|
|
24645
|
-
|
|
24949
|
+
const publishedOutputNames = collectPublishedOutputNames(template.Outputs, conditions);
|
|
24950
|
+
let outputsPassCompleted = false;
|
|
24951
|
+
try {
|
|
24952
|
+
for (const [outputKey, output] of Object.entries(template.Outputs)) {
|
|
24953
|
+
if (isOutputSuppressedByCondition(output, conditions)) {
|
|
24954
|
+
this.logger.debug(`Skipping output ${outputKey} — condition ${output.Condition} is false`);
|
|
24955
|
+
continue;
|
|
24956
|
+
}
|
|
24957
|
+
try {
|
|
24958
|
+
outputs[outputKey] = await this.resolver.resolve(output.Value, context);
|
|
24959
|
+
} catch (error) {
|
|
24960
|
+
this.handleOutputResolutionFailure(error, outputKey, outputs);
|
|
24961
|
+
}
|
|
24646
24962
|
}
|
|
24647
|
-
|
|
24648
|
-
|
|
24649
|
-
|
|
24650
|
-
|
|
24651
|
-
|
|
24652
|
-
|
|
24653
|
-
|
|
24654
|
-
|
|
24963
|
+
for (const [outputKey, output] of Object.entries(template.Outputs)) {
|
|
24964
|
+
if (isOutputSuppressedByCondition(output, conditions)) continue;
|
|
24965
|
+
if (!output.Export?.Name) continue;
|
|
24966
|
+
const value = outputs[outputKey];
|
|
24967
|
+
if (value === void 0) continue;
|
|
24968
|
+
const nameSecrets = /* @__PURE__ */ new Map();
|
|
24969
|
+
let exportName;
|
|
24970
|
+
try {
|
|
24971
|
+
try {
|
|
24972
|
+
exportName = typeof output.Export.Name === "string" ? output.Export.Name : await this.resolver.resolve(output.Export.Name, {
|
|
24973
|
+
...context,
|
|
24974
|
+
recordedSecretValues: nameSecrets
|
|
24975
|
+
});
|
|
24976
|
+
} finally {
|
|
24977
|
+
for (const [plaintext, expression] of nameSecrets) context.recordedSecretValues?.set(plaintext, expression);
|
|
24655
24978
|
}
|
|
24979
|
+
} catch (error) {
|
|
24980
|
+
this.handleOutputResolutionFailure(error, outputKey, outputs);
|
|
24981
|
+
continue;
|
|
24982
|
+
}
|
|
24983
|
+
if (typeof exportName !== "string") continue;
|
|
24984
|
+
const exposure = exportNameSecretExposure(exportName, nameSecrets, context.recordedSecretValues);
|
|
24985
|
+
if (exposure) this.logger.warn(secretBearingExportNameWarning(outputKey, exportName, exposure));
|
|
24986
|
+
else if (isExportAliasCollision(exportName, outputKey, publishedOutputNames)) this.logger.warn(exportAliasCollisionWarning(outputKey, exportName));
|
|
24987
|
+
else {
|
|
24988
|
+
outputs[exportName] = value;
|
|
24989
|
+
this.outputsTemplateSource[exportName] = output.Value;
|
|
24656
24990
|
}
|
|
24657
|
-
} catch (error) {
|
|
24658
|
-
if (this.options.strictGetAtt) throw new Error(`Failed to resolve output ${outputKey}: ${error instanceof Error ? error.message : String(error)} (--strict-getatt promotes output resolution failures to deploy errors; drop the flag to warn and skip the output instead)`, { cause: error });
|
|
24659
|
-
this.logger.warn(`Failed to resolve output ${outputKey}: ${String(error)}`);
|
|
24660
|
-
outputs[outputKey] = void 0;
|
|
24661
24991
|
}
|
|
24992
|
+
outputsPassCompleted = true;
|
|
24993
|
+
} finally {
|
|
24994
|
+
if (context.recordedSecretValues) for (const [value, expr] of context.recordedSecretValues) this.outputSecrets.set(value, expr);
|
|
24995
|
+
if (!outputsPassCompleted) this.outputsSourceUsable = false;
|
|
24996
|
+
}
|
|
24997
|
+
for (const [outputKey, output] of Object.entries(template.Outputs)) {
|
|
24998
|
+
if (!publishedOutputNames.has(outputKey)) continue;
|
|
24999
|
+
this.outputsTemplateSource[outputKey] = output.Value;
|
|
24662
25000
|
}
|
|
24663
|
-
if (context.recordedSecretValues) for (const [value, expr] of context.recordedSecretValues) this.outputSecrets.set(value, expr);
|
|
24664
|
-
for (const [outputKey, output] of Object.entries(template.Outputs)) this.outputsTemplateSource[outputKey] = output.Value;
|
|
24665
25001
|
return outputs;
|
|
24666
25002
|
}
|
|
24667
25003
|
buildDisplayOutputs(template, resolvedOutputs) {
|
|
@@ -24676,5 +25012,5 @@ var DeployEngine = class {
|
|
|
24676
25012
|
};
|
|
24677
25013
|
|
|
24678
25014
|
//#endregion
|
|
24679
|
-
export {
|
|
24680
|
-
//# sourceMappingURL=deploy-engine-
|
|
25015
|
+
export { getAccountInfo as $, isCdkdError as $n, runDockerForeground as $t, cyan as A, getAwsClients as An, S3StateBackend as At, stateKeySecretExposure as B, LocalStartServiceError as Bn, escapeRegExp$1 as Bt, makeCanonicalizePropertiesFn as C, canonicalizeRegion as Cn, DiffCalculator as Ct, renderStatefulReason as D, clearBucketRegionCache as Dn, DagBuilder as Dt, isStatefulRecreateTargetSync as E, processStackMessages as En, withRetry as Et, collectDeclaredOutputNames as F, ConfigError as Fn, WorkGraph as Ft, findActionableSilentDrops as G, ProvisioningError as Gn, getBootstrapMarkerKey as Gt, collectInlinePolicyNamesManagedBySiblings as H, MissingCdkCliError as Hn, AssetModeResolver as Ht, collectPublishedOutputNames as I, DependencyError as In, buildAssetRedirectMap as It, slowCcOperationTimeoutMs as J, StackHasActiveImportsError as Jn, validateContainerRepoName as Jt, findSilentDropProperties as K, ResourceTimeoutError as Kn, parseBootstrapMarker as Kt, exportAliasCollisionScrubWarning as L, DeployCancelledError as Ln, createAssetRedirectResolver as Lt, green as M, setAwsClients as Mn, shouldRetainResource as Mt, red as N, AssetError as Nn, AssetPublisher as Nt, formatResourceLine as O, resolveBucketRegion as On, TemplateParser as Ot, yellow as P, CdkdError as Pn, stringifyValue as Pt, cfnRefValueFromPhysicalId as Q, formatError as Qn, getDockerCmd as Qt, isExportAliasCollision as R, LocalInvokeBuildError as Rn, loadPublishableAssetManifest as Rt, unsupportedFinalSnapshotError as S, PARTITION_TABLE as Sn, applyRoleArnIfSet as St, MULTI_REGION_RECREATE_BLOCKED_TYPES as T, AssemblyReader as Tn, describeTypeWithThrottleRetry as Tt, clearOnUpdateRemoval as U, NestedStackChildDirectDestroyError as Un, BOOTSTRAP_MARKER_PREFIX as Ut, IAMRoleProvider as V, LockError as Vn, stripControlChars as Vt, ProviderRegistry as W, PartialFailureError as Wn, ensureAssetStorage as Wt, isTerminationProtectionPropagationError as X, StateError as Xn, buildDockerImage as Xt, disableInstanceApiTermination as Y, StackTerminationProtectionError as Yn, buildDenyExternalAccessPolicy as Yt, IntrinsicFunctionResolver as Z, SynthesisError as Zn, formatDockerLoginError as Zt, buildFinalSnapshotIdentifier as _, CFN_TEMPLATE_URL_LIMIT as _n, s3BucketArn as _t, DeploymentEventsStore as a, getDefaultStateBucketName as an, markNonRetryable as ar, coerceCfnBoolean as at, isFinalSnapshotError as b, uploadCfnTemplate as bn, s3BucketRegionalDomainName as bt, replayFailedOperations as c, resolveAutoAssetStorage as cn, readConfigString as ct, deleteSkipReason as d, resolveStateBucketWithDefault as dn, requireConfigObject as dt, runDockerStreaming as en, normalizeAwsError as er, refStateLookupFromResource as et, withResourceDeadline as f, resolveStateBucketWithDefaultAndSource as fn, requireConfigString as ft, PRE_DELETE_SNAPSHOT_TYPES as g, CFN_TEMPLATE_BODY_LIMIT as gn, scrubResourceRecord as gt, ATOMIC_FINAL_SNAPSHOT_TYPES as h, warnDeprecatedNoPrefixCliFlag as hn, redactSecretsForState as ht, DeploymentEventsReader as i, synthesisStatusMessage as in, isThrottlingError as ir, assertRegionMatch as it, gray as j, resetAwsClients as jn, rebuildClientForBucketRegion as jt, bold as k, AwsClients as kn, LockManager as kt, replayRollback as l, resolveCaptureObservedState as ln, replayWarn as lt, computeImplicitDeleteEdges as m, stateBucketExistenceConfirmed as mn, TEMPLATE_SOURCED_RULES as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, getDockerImageBySourceHash as nn, isMarkedNonRetryable as nr, normalizeAwsTagsToCfn as nt, planFailedOps as o, getLegacyStateBucketName as on, __exportAll as or, configBooleanRefusal as ot, IMPLICIT_DELETE_DEPENDENCIES as p, resolveUseCdkBootstrapAssets as pn, STATE_SOURCED_CROSS_GENERATION_RULES as pt, CloudControlProvider as q, ResourceUpdateNotSupportedError as qn, validateAssetBucketName as qt, DeployEngine as r, Synthesizer as rn, isRetryableTransientError as rr, resolveExplicitPhysicalId as rt, planRollback as s, resolveApp as sn, configStringRefusal as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, AssetManifestLoader as tn, withErrorHandling as tr, WAFv2WebACLProvider as tt, UNSPECIFIED_SKIP_REASON as u, resolveSkipPrefix as un, requireConfigArray as ut, ccRoutedFinalSnapshotError as v, MIGRATE_TMP_PREFIX as vn, s3BucketDomainName as vt, extractDeploymentEventError as w, derivePartitionAndUrlSuffix as wn, INTRINSIC_KEYS as wt, refusesFinalSnapshot as x, expectedOwnerParam as xn, s3BucketWebsiteUrl as xt, createPreDeleteFinalSnapshot as y, findLargeInlineResources as yn, s3BucketDualStackDomainName as yt, secretBearingStateKeyWarning as z, LocalMigrateError as zn, rewriteTemplateAssetReferences as zt };
|
|
25016
|
+
//# sourceMappingURL=deploy-engine-rYTNzst0.js.map
|