@go-to-k/cdkd 0.288.3 → 0.288.5
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-CQrHElrl.js → asg-provider-C_AHAmgD.js} +2 -2
- package/dist/{asg-provider-CQrHElrl.js.map → asg-provider-C_AHAmgD.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-CfxcC3q3.js → deploy-engine-Dsf9bTqH.js} +1085 -215
- package/dist/deploy-engine-Dsf9bTqH.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-BXH3FeeV.js → program-CMhpftan.js} +558 -116
- package/dist/{program-BXH3FeeV.js.map → program-CMhpftan.js.map} +1 -1
- package/dist/{version-CEyZvTcB.js → version-BFmq8S23.js} +2 -2
- package/dist/{version-CEyZvTcB.js.map → version-BFmq8S23.js.map} +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-CfxcC3q3.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { _ as withStackName, d as applyDefaultNameForFallback, f as generateResourceName, h as looksLikeCdkdGeneratedName, m as getCurrentStackName, n as getLogger, p as generateResourceNameWithFallback, r as isStdoutReservedForPayload, s as getLiveRenderer } from "./logger-Dw-3y48G.js";
|
|
2
2
|
import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
|
|
3
|
-
import { t as getCdkdVersion } from "./version-
|
|
3
|
+
import { t as getCdkdVersion } from "./version-BFmq8S23.js";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetBucketReplicationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -102,6 +102,7 @@ const IAM_PROPAGATION_ERROR_MESSAGE_PATTERNS = [
|
|
|
102
102
|
"required permissions for: ENHANCED_MONITORING",
|
|
103
103
|
"Caught ServiceAccessDeniedException",
|
|
104
104
|
"permissions required to assume the role",
|
|
105
|
+
"does not have a trust relationship allowing",
|
|
105
106
|
"authorized to assume the provided role",
|
|
106
107
|
"not authorized to access the Log Destination",
|
|
107
108
|
"Cannot access stream",
|
|
@@ -4017,6 +4018,18 @@ async function expectedOwnerParam(client) {
|
|
|
4017
4018
|
* A caller whose value has a KNOWN ASCII charset (a stack name, an AWS region)
|
|
4018
4019
|
* should pass `asciiOnly`, which is a positive allowlist and therefore has no
|
|
4019
4020
|
* such residual at all.
|
|
4021
|
+
*
|
|
4022
|
+
* ONE CALLER DELIBERATELY GOES WIDER, and it is recorded here so an editor of
|
|
4023
|
+
* the residual note above knows a second module now disagrees with it.
|
|
4024
|
+
* `src/deployment/outputs-export-alias.ts` deletes a class derived from
|
|
4025
|
+
* `\p{Cc}` / `\p{Cf}` / `\p{Zl}` / `\p{Zp}` /
|
|
4026
|
+
* `\p{Default_Ignorable_Code_Point}`, because on THAT path the subject is a possibly
|
|
4027
|
+
* secret-bearing name in an operator's log: a plaintext split by a zero-width
|
|
4028
|
+
* character is READ as if it were contiguous, so it is disclosed without any
|
|
4029
|
+
* paste, and the command-forgery reasoning above does not transfer (issue
|
|
4030
|
+
* [#2874](https://github.com/go-to-k/cdkd/issues/2874)). Nothing here changes
|
|
4031
|
+
* -- widening this helper would alter every caller that merely wants a
|
|
4032
|
+
* terminal-safe string.
|
|
4020
4033
|
*/
|
|
4021
4034
|
function displaySafe(value, opts) {
|
|
4022
4035
|
if (value === void 0 || value === null) return "";
|
|
@@ -12245,13 +12258,45 @@ const TEMPLATE_SOURCED_RULES = {
|
|
|
12245
12258
|
trustAnyExpression: false,
|
|
12246
12259
|
sourceIsSameGeneration: false
|
|
12247
12260
|
};
|
|
12248
|
-
/**
|
|
12261
|
+
/**
|
|
12262
|
+
* An AWS readback projected from THIS record's own persisted STATE bag.
|
|
12263
|
+
*
|
|
12264
|
+
* Does NOT fail closed. This is the constant a caller reaches for when it knows
|
|
12265
|
+
* the SHAPE of its two bags and nothing about where the result lands —
|
|
12266
|
+
* `cdkd drift`'s two writers and `cdkd import`'s capture pass it, and so does
|
|
12267
|
+
* `DeployEngine.redactOperationsForJournal` for the journal's `previousState`
|
|
12268
|
+
* (issue [#2886](https://github.com/go-to-k/cdkd/issues/2886): a REPLAYED
|
|
12269
|
+
* baseline must not gain masks a rollback restore then persists). See
|
|
12270
|
+
* {@link STATE_SOURCED_BASELINE_RULES} for the one that does, and
|
|
12271
|
+
* `failClosedOnUncertifiedPositions` for why the difference is declared rather
|
|
12272
|
+
* than derived.
|
|
12273
|
+
*/
|
|
12249
12274
|
const STATE_SOURCED_READBACK_RULES = {
|
|
12250
12275
|
descendArrays: false,
|
|
12251
12276
|
trustAnyExpression: true,
|
|
12252
12277
|
sourceIsSameGeneration: true
|
|
12253
12278
|
};
|
|
12254
12279
|
/**
|
|
12280
|
+
* {@link STATE_SOURCED_READBACK_RULES} for a caller that KNOWS its bag becomes
|
|
12281
|
+
* a drift BASELINE — `observedProperties` and nothing else (issue
|
|
12282
|
+
* [#2852](https://github.com/go-to-k/cdkd/issues/2852)).
|
|
12283
|
+
*
|
|
12284
|
+
* Identical on the three shape flags, so every relaxation and refusal the
|
|
12285
|
+
* readback path already had applies unchanged; the only difference is that a
|
|
12286
|
+
* position this pass cannot certify is written as {@link SECRET_MASK} rather
|
|
12287
|
+
* than as the DECRYPTED readback. Passed by `cdkd state refresh-observed` and
|
|
12288
|
+
* derived by {@link scrubResourceRecord} for the observed bag — the deploy's
|
|
12289
|
+
* `drainObservedCaptures` baseline reaches it that way. `cdkd import`'s own
|
|
12290
|
+
* capture still passes the non-failing constant and keeps the residue; moving
|
|
12291
|
+
* it is a one-constant change in `src/cli/commands/import.ts`.
|
|
12292
|
+
*/
|
|
12293
|
+
const STATE_SOURCED_BASELINE_RULES = {
|
|
12294
|
+
descendArrays: false,
|
|
12295
|
+
trustAnyExpression: true,
|
|
12296
|
+
sourceIsSameGeneration: true,
|
|
12297
|
+
failClosedOnUncertifiedPositions: true
|
|
12298
|
+
};
|
|
12299
|
+
/**
|
|
12255
12300
|
* A STATE source that is no longer this bag's own generation — `cdkd scrub`'s
|
|
12256
12301
|
* `observedProperties` walk (issue #1917 review).
|
|
12257
12302
|
*
|
|
@@ -12397,7 +12442,7 @@ function isSecretExpressionByVerdictOrSpelling(expression) {
|
|
|
12397
12442
|
* so the position source is present with no map beside it.
|
|
12398
12443
|
*
|
|
12399
12444
|
* `cdkd state refresh-observed` and the deploy's `drainObservedCaptures` are
|
|
12400
|
-
* NOT affected: they take `
|
|
12445
|
+
* NOT affected: they take a `STATE_SOURCED_*` readback constant, which sets
|
|
12401
12446
|
* `sourceIsSameGeneration`, so {@link refuseUncertifiedReadbackPositions}
|
|
12402
12447
|
* restores the source even under the old strict class.
|
|
12403
12448
|
*
|
|
@@ -13312,7 +13357,7 @@ function redactByPath(bag, source, secrets, rules, secretExpressions, bagIsSameG
|
|
|
13312
13357
|
}
|
|
13313
13358
|
if (rules.descendArrays && bag.length === source.length) return bag.map((item, i) => redactByPath(item, source[i], secrets, rules, secretExpressions, bagIsSameGeneration));
|
|
13314
13359
|
}
|
|
13315
|
-
if (isPlainObject$2(bag) && isPlainObject$2(source)) {
|
|
13360
|
+
if (isPlainObject$2(bag) && hasPlainPrototype(bag) && isPlainObject$2(source)) {
|
|
13316
13361
|
const out = Object.create(null);
|
|
13317
13362
|
for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? redactByPath(v, source[k], secrets, rules, secretExpressions, bagIsSameGeneration) : redactSecretsForState(v, secrets);
|
|
13318
13363
|
return out;
|
|
@@ -13323,7 +13368,10 @@ function redactByPath(bag, source, secrets, rules, secretExpressions, bagIsSameG
|
|
|
13323
13368
|
* Is this rules constant one whose BAG is an AWS readback and whose SOURCE is a
|
|
13324
13369
|
* persisted STATE bag?
|
|
13325
13370
|
*
|
|
13326
|
-
* Today that is {@link STATE_SOURCED_READBACK_RULES}
|
|
13371
|
+
* Today that is {@link STATE_SOURCED_READBACK_RULES} and its fail-closed twin
|
|
13372
|
+
* {@link STATE_SOURCED_BASELINE_RULES}, which differ on nothing this predicate
|
|
13373
|
+
* reads (issue #2852 added a DESTINATION flag, not a shape one): the path where
|
|
13374
|
+
* the
|
|
13327
13375
|
* secrets map can be EMPTY by construction (nothing was resolved), so the value
|
|
13328
13376
|
* scan has no needles and POSITION is the only mechanism left. Derived from the
|
|
13329
13377
|
* flags rather than compared against the constant so a future one with the same
|
|
@@ -13819,6 +13867,23 @@ function unkeyedArrayPairsByAnchors(bag, source) {
|
|
|
13819
13867
|
*/
|
|
13820
13868
|
const POSITION_DECIDED = Symbol("position decided by a position pass");
|
|
13821
13869
|
/**
|
|
13870
|
+
* Marks a STRING leaf {@link refuseUncertifiedReadbackPositions} REFUSED — a
|
|
13871
|
+
* position whose source subtree proves a dynamic reference lives there while
|
|
13872
|
+
* the walk could not pair the two sides, so the readback value at it may be a
|
|
13873
|
+
* decrypted secret (issue
|
|
13874
|
+
* [#2852](https://github.com/go-to-k/cdkd/issues/2852)).
|
|
13875
|
+
*
|
|
13876
|
+
* A THIRD state, not a second spelling of {@link POSITION_DECIDED}, and the
|
|
13877
|
+
* difference is what keeps the fail-closed change from REGRESSING the derived
|
|
13878
|
+
* needles issue #2012 added. `POSITION_DECIDED` tells
|
|
13879
|
+
* {@link preferPositionDecisions} "this leaf is mine, the scan may not touch
|
|
13880
|
+
* it"; a refusal makes the opposite claim — the pass has NO answer here, only
|
|
13881
|
+
* the knowledge that the raw value is unsafe. So the scan still gets to win at
|
|
13882
|
+
* such a leaf (a derived needle NAMES the expression, which is strictly better
|
|
13883
|
+
* than a mask), and the mask stands only where nothing else spoke.
|
|
13884
|
+
*/
|
|
13885
|
+
const POSITION_UNCERTIFIED = Symbol("position refused by a position pass");
|
|
13886
|
+
/**
|
|
13822
13887
|
* Record one (plaintext -> expression) pair, or strike the plaintext out.
|
|
13823
13888
|
*
|
|
13824
13889
|
* Below {@link MIN_NEEDLE_LENGTH} nothing is recorded, and this floor DECIDES
|
|
@@ -14080,11 +14145,13 @@ function asIndex(marks, index) {
|
|
|
14080
14145
|
* newly extending #2427 to the EMPTY-map path, where the unchanged-resource
|
|
14081
14146
|
* `drainObservedCaptures` baseline lives and where `cdkd drift --revert` pushes
|
|
14082
14147
|
* the result to the live resource. With the guard a non-plain leaf falls
|
|
14083
|
-
* through to `refused`. That is the position passes' own answer
|
|
14084
|
-
* bag by identity
|
|
14085
|
-
*
|
|
14086
|
-
*
|
|
14087
|
-
*
|
|
14148
|
+
* through to `refused`. That is the position passes' own answer, which is the
|
|
14149
|
+
* bag by identity: their object arm carried no prototype guard of its own until
|
|
14150
|
+
* issue [#2869](https://github.com/go-to-k/cdkd/issues/2869), so a non-plain
|
|
14151
|
+
* leaf whose source subtree carries a reference WAS already flattened one
|
|
14152
|
+
* function earlier and this guard could only keep a `{}` intact. Both halves
|
|
14153
|
+
* are guarded now; the remaining copy of the defect is the VALUE scan's own
|
|
14154
|
+
* walk, which is issue #2427 and a different pass.
|
|
14088
14155
|
*
|
|
14089
14156
|
* The net effect is byte-identical to the FIRST ordering on every input where
|
|
14090
14157
|
* the un-certification did not fire — which is the whole point: it keeps that
|
|
@@ -14098,7 +14165,9 @@ function preferPositionDecisions(scanned, refused, bag, marks, inferred) {
|
|
|
14098
14165
|
}
|
|
14099
14166
|
if (Array.isArray(bag) && Array.isArray(refused) && Array.isArray(scanned) && refused.length === bag.length && scanned.length === bag.length) return refused.map((item, i) => preferPositionDecisions(scanned[i], item, bag[i], asIndex(marks, i), inferred));
|
|
14100
14167
|
if (typeof bag !== "string" || marks === POSITION_DECIDED) return refused;
|
|
14101
|
-
|
|
14168
|
+
const scanDecision = scanned === bag ? inferred.get(bag) ?? scanned : scanned;
|
|
14169
|
+
if (marks === POSITION_UNCERTIFIED && !(typeof scanDecision === "string" && isSingleDynamicReferenceToken(scanDecision))) return refused;
|
|
14170
|
+
return scanDecision;
|
|
14102
14171
|
}
|
|
14103
14172
|
/**
|
|
14104
14173
|
* DERIVED NEEDLES (issue [#2012](https://github.com/go-to-k/cdkd/issues/2012)):
|
|
@@ -14168,7 +14237,7 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14168
14237
|
poisoned: /* @__PURE__ */ new Set(),
|
|
14169
14238
|
inferred: /* @__PURE__ */ new Set()
|
|
14170
14239
|
};
|
|
14171
|
-
refuseUncertifiedReadbackPositions(bag, source, secrets, collector);
|
|
14240
|
+
refuseUncertifiedReadbackPositions(bag, source, secrets, false, collector);
|
|
14172
14241
|
if (collector.needles.size === 0) return void 0;
|
|
14173
14242
|
const certain = /* @__PURE__ */ new Map();
|
|
14174
14243
|
const inferred = /* @__PURE__ */ new Map();
|
|
@@ -14179,6 +14248,149 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14179
14248
|
};
|
|
14180
14249
|
}
|
|
14181
14250
|
/**
|
|
14251
|
+
* FAIL CLOSED over one readback subtree the position walk could not certify
|
|
14252
|
+
* (issue [#2852](https://github.com/go-to-k/cdkd/issues/2852)).
|
|
14253
|
+
*
|
|
14254
|
+
* Every STRING leaf the source cannot account for becomes
|
|
14255
|
+
* {@link SECRET_MASK} — at EVERY position it occupies, including a node the bag
|
|
14256
|
+
* reaches twice; see the memo below. So does a BINARY leaf, whose bytes are a
|
|
14257
|
+
* secret in the clear once `JSON.stringify` writes them. Everything else is
|
|
14258
|
+
* kept. Called from
|
|
14259
|
+
* {@link refuseUncertifiedReadbackPositions} — through
|
|
14260
|
+
* {@link refuseAgainstSource}, and DIRECTLY from its keyed-array arm, which
|
|
14261
|
+
* hoists the literal set and re-spells the `failClosed` test — and only where
|
|
14262
|
+
* that walk has already established
|
|
14263
|
+
* BOTH halves of the evidence: the SOURCE subtree at this position spells a
|
|
14264
|
+
* dynamic reference (so the template says a secret lives here), and the two
|
|
14265
|
+
* sides cannot be paired (so no position can say WHICH leaf holds its resolved
|
|
14266
|
+
* form). Before this, every such branch returned the bag — the decrypted
|
|
14267
|
+
* readback — verbatim.
|
|
14268
|
+
*
|
|
14269
|
+
* "The source cannot account for" is the whole claim, and it is deliberately
|
|
14270
|
+
* WEAKER than "no plaintext survives": a leaf the source spells verbatim is
|
|
14271
|
+
* kept, so a readback that echoes a template literal back keeps it. What the
|
|
14272
|
+
* pass guarantees is that no leaf survives on the strength of the walk having
|
|
14273
|
+
* given up.
|
|
14274
|
+
*
|
|
14275
|
+
* WHY A MASK RATHER THAN THE SOURCE. Substituting the source is what the
|
|
14276
|
+
* certified rows do, and it is exactly what the array arm's own comment (and
|
|
14277
|
+
* the issue #1915 fences) refuse here: with no pairing, writing the source
|
|
14278
|
+
* fabricates baseline content AWS never reported, which `cdkd drift --revert`
|
|
14279
|
+
* then pushes to the live resource. A mask fabricates no content — it keeps
|
|
14280
|
+
* the bag's SHAPE, adds no key, no element and no scalar-over-container — and
|
|
14281
|
+
* `SECRET_MASK` is already a first-class persisted state with its own
|
|
14282
|
+
* downstream guards (`drift.ts`'s `collectSecretMaskPaths` /
|
|
14283
|
+
* `preserveLiveValuesAtMaskedLeaves`, `runAccept`'s refusal,
|
|
14284
|
+
* `rollback-executor.ts`'s `refuseMaskedReplayBaseline`), because the
|
|
14285
|
+
* mask-only channel (issue #2274) already puts one there.
|
|
14286
|
+
*
|
|
14287
|
+
* WHY STRINGS ONLY. A recorded secret is a `string` by the type of
|
|
14288
|
+
* {@link RecordedSecretValues}, so a number, a boolean or `null` cannot BE a
|
|
14289
|
+
* resolved secret and masking one would only cost drift a comparison. A
|
|
14290
|
+
* NON-PLAIN object (a `Date` an AWS SDK readback carries, a `Buffer`) is
|
|
14291
|
+
* returned BY IDENTITY for the same reason plus a second one: rebuilding it
|
|
14292
|
+
* from its own enumerable keys yields `{}` — the corruption of issue
|
|
14293
|
+
* [#2869](https://github.com/go-to-k/cdkd/issues/2869).
|
|
14294
|
+
*
|
|
14295
|
+
* A leaf that IS a whole `{{resolve:...}}` token is kept: it is an expression
|
|
14296
|
+
* AWS echoed back unresolved, not plaintext, and replacing it with a mask would
|
|
14297
|
+
* DESTROY a value `cdkd drift` can re-resolve. WHOLE, not "contains one" — that
|
|
14298
|
+
* wider test spared `postgres://admin:<plaintext>@{{resolve:ssm-secure:/h}}`,
|
|
14299
|
+
* where the embedded token vouched for a leaf that was mostly the decrypted
|
|
14300
|
+
* secret. The residual is the issue #1917 shape — a plaintext that merely LOOKS
|
|
14301
|
+
* like a token — which every arm of this module already trusts.
|
|
14302
|
+
*
|
|
14303
|
+
* SO IS A LEAF THE SOURCE SUBTREE ITSELF SPELLS, and this is what keeps the
|
|
14304
|
+
* fail-closed change from emptying an ordinary drift baseline. `sourceLiterals`
|
|
14305
|
+
* is {@link wholeStringLeavesOf} over the SOURCE at the refused position — the
|
|
14306
|
+
* literal frame of an `Fn::Join`, the anchor values of an array AWS reordered,
|
|
14307
|
+
* every ordinary property beside the reference. A value the template SPELLS is
|
|
14308
|
+
* not the resolved form of a reference, so masking it buys nothing; and where
|
|
14309
|
+
* it coincides with one, that plaintext is already sitting in the record's own
|
|
14310
|
+
* `properties`, so the copy in the readback is not the disclosure. Scoped to
|
|
14311
|
+
* the SOURCE AT THE REFUSED POSITION rather than the whole record on purpose: a
|
|
14312
|
+
* coincidence three properties away is not evidence about this one. Read that
|
|
14313
|
+
* literally — when an ARRAY refuses element by element the refused position is
|
|
14314
|
+
* the array, so a SIBLING element's literal does spare a leaf. That is the
|
|
14315
|
+
* intended granularity (the elements are peers of one list AWS returned
|
|
14316
|
+
* together, and the pairing that failed is between the two LISTS), and it is
|
|
14317
|
+
* stated because "subtree" reads narrower than the code is.
|
|
14318
|
+
*
|
|
14319
|
+
* OVER-MASKING IS THE REMAINING COST AND IT IS THE INTENDED DIRECTION: a value
|
|
14320
|
+
* AWS NORMALISED (`us-east-1` returned as `US-EAST-1`) no longer matches the
|
|
14321
|
+
* source and is masked with the secret, because nothing distinguishes them once
|
|
14322
|
+
* the pairing is gone. That is phantom drift rather than a disclosure — the
|
|
14323
|
+
* same way this module chooses to be wrong at
|
|
14324
|
+
* {@link mixedLeafMayCarryPublicReference}.
|
|
14325
|
+
*
|
|
14326
|
+
* `mark` is {@link refuseUncertifiedReadbackPositions}'s MARK MODE, threaded
|
|
14327
|
+
* so the parallel tree keeps the same shape: {@link POSITION_UNCERTIFIED}
|
|
14328
|
+
* lands wherever the substituting pass puts a mask, and the bag's own value
|
|
14329
|
+
* everywhere else — which is that mode's contract.
|
|
14330
|
+
*/
|
|
14331
|
+
function refuseUncertifiedSubtree(value, sourceLiterals, mark, seen = /* @__PURE__ */ new Map()) {
|
|
14332
|
+
if (typeof value === "string") {
|
|
14333
|
+
if (isSingleDynamicReferenceToken(value) || sourceLiterals.has(value)) return value;
|
|
14334
|
+
if (value === "") return value;
|
|
14335
|
+
return mark ? POSITION_UNCERTIFIED : "***";
|
|
14336
|
+
}
|
|
14337
|
+
if (value === null || typeof value !== "object") return value;
|
|
14338
|
+
const memo = seen.get(value);
|
|
14339
|
+
if (memo !== void 0) return memo;
|
|
14340
|
+
if (Array.isArray(value)) {
|
|
14341
|
+
const out = [];
|
|
14342
|
+
seen.set(value, out);
|
|
14343
|
+
for (const item of value) out.push(refuseUncertifiedSubtree(item, sourceLiterals, mark, seen));
|
|
14344
|
+
return out;
|
|
14345
|
+
}
|
|
14346
|
+
if (isPlainObject$2(value) && hasPlainPrototype(value)) {
|
|
14347
|
+
const out = Object.create(null);
|
|
14348
|
+
seen.set(value, out);
|
|
14349
|
+
for (const [k, v] of Object.entries(value)) out[k] = refuseUncertifiedSubtree(v, sourceLiterals, mark, seen);
|
|
14350
|
+
return out;
|
|
14351
|
+
}
|
|
14352
|
+
if (ArrayBuffer.isView(value)) return mark ? POSITION_UNCERTIFIED : "***";
|
|
14353
|
+
seen.set(value, value);
|
|
14354
|
+
return value;
|
|
14355
|
+
}
|
|
14356
|
+
/**
|
|
14357
|
+
* {@link refuseUncertifiedSubtree} over a bag whose SOURCE is in hand, so the
|
|
14358
|
+
* literal set can never be built from anything but the source at the SAME
|
|
14359
|
+
* position.
|
|
14360
|
+
*
|
|
14361
|
+
* NOT the only spelling, and an earlier revision of this sentence said it was.
|
|
14362
|
+
* The keyed-array arm calls {@link refuseUncertifiedSubtree} DIRECTLY, because
|
|
14363
|
+
* it hoists the literal set out of its `bag.map` — so it also re-spells the
|
|
14364
|
+
* `failClosed` test this function owns. A future edit that drops that
|
|
14365
|
+
* re-spelling drops the DESTINATION check with it, which is why the two are
|
|
14366
|
+
* named here rather than left to be noticed.
|
|
14367
|
+
*/
|
|
14368
|
+
function refuseAgainstSource(bag, source, failClosed, mark) {
|
|
14369
|
+
if (failClosed !== true) return bag;
|
|
14370
|
+
return refuseUncertifiedSubtree(bag, wholeStringLeavesOf(source), mark);
|
|
14371
|
+
}
|
|
14372
|
+
/**
|
|
14373
|
+
* Does any element of `source` that the bag did NOT pair carry a dynamic
|
|
14374
|
+
* reference (issue [#2852](https://github.com/go-to-k/cdkd/issues/2852))?
|
|
14375
|
+
*
|
|
14376
|
+
* The evidence that licenses refusing the bag's own unpaired elements in the
|
|
14377
|
+
* IDENTITY-KEYED array arm. An identity key that does not round-trip
|
|
14378
|
+
* byte-identically — AWS case-normalises a `Name`, or expands one to an ARN —
|
|
14379
|
+
* drops its element to `partner === undefined`, and the element the source
|
|
14380
|
+
* spells as a reference is then left over with nothing pointing at it, so its
|
|
14381
|
+
* resolved plaintext is somewhere in the unpaired remainder.
|
|
14382
|
+
*
|
|
14383
|
+
* The converse is why this is a QUESTION rather than a blanket refusal: when
|
|
14384
|
+
* every reference-bearing source element DID find its partner, an extra bag
|
|
14385
|
+
* element is a peer AWS added (another `Environment` entry) and carries no
|
|
14386
|
+
* secret this source can account for. Refusing those would mask ordinary
|
|
14387
|
+
* readback content for no evidence, which is the same trade the object arm's
|
|
14388
|
+
* extra-KEY branch declines to make.
|
|
14389
|
+
*/
|
|
14390
|
+
function unpairedSourceCarriesReference(source, key, bagIdentities) {
|
|
14391
|
+
return source.some((item) => !bagIdentities.has(item[key]) && subtreeHasDynamicReference(item));
|
|
14392
|
+
}
|
|
14393
|
+
/**
|
|
14182
14394
|
* Refuse to persist a readback leaf the path pass could not CERTIFY, at any
|
|
14183
14395
|
* position the STATE source proves is secret-bearing (issue #1926 review).
|
|
14184
14396
|
*
|
|
@@ -14200,15 +14412,42 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14200
14412
|
* ...the same MIXED leaf inside a PAIRED element LEAK take source
|
|
14201
14413
|
* `['--pw', '{{resolve:...}}']` (no identity key) LEAK take source*
|
|
14202
14414
|
* `[{Field, Val: '{{resolve:...}}'}]` (no `Name`) LEAK take source*
|
|
14203
|
-
* ...either of those, but REORDERED / normalised LEAK
|
|
14204
|
-
* an UNPAIRED element
|
|
14205
|
-
* an
|
|
14415
|
+
* ...either of those, but REORDERED / normalised LEAK MASK (#2852)
|
|
14416
|
+
* an UNPAIRED element, source reference left over LEAK needle | MASK
|
|
14417
|
+
* an UNPAIRED element, every source reference paired LEAK needle (#2012)
|
|
14418
|
+
* a RESHAPED container / added wrapper level LEAK MASK (#2852)
|
|
14419
|
+
* a source leaf promoted to a container LEAK MASK (#2852)
|
|
14420
|
+
* a RAW `Fn::Join` source vs a STRING readback LEAK MASK (#2846)
|
|
14421
|
+
* an observed KEY the source does not carry LEAK needle | LEAK
|
|
14422
|
+
* a `Date` under a reference-bearing source subtree `{}` kept (#2869)
|
|
14206
14423
|
* whole `{{resolve:...}}` token ok ok
|
|
14207
14424
|
* `Environment[]` keyed by `Name` (issue #1915) ok ok
|
|
14208
14425
|
* PUBLIC ssm MIXED leaf, POPULATED map ok ok
|
|
14209
14426
|
* PUBLIC ssm MIXED leaf, EMPTY map ok over-redacts
|
|
14210
14427
|
* ```
|
|
14211
14428
|
*
|
|
14429
|
+
* MASK rows are the FAIL-CLOSED change of issue
|
|
14430
|
+
* [#2852](https://github.com/go-to-k/cdkd/issues/2852). Every branch this walk
|
|
14431
|
+
* could not certify used to `return bag` — the decrypted readback, verbatim —
|
|
14432
|
+
* so "cannot pair" and "safe to persist" were the same answer. They are now
|
|
14433
|
+
* {@link refuseUncertifiedSubtree}, whose doc argues why a mask rather than the
|
|
14434
|
+
* source and why STRING leaves only. `needle | MASK` means the derived needles
|
|
14435
|
+
* of issue #2012 are consulted FIRST and the mask stands only where they had
|
|
14436
|
+
* nothing to say ({@link preferPositionDecisions}), so no row this table
|
|
14437
|
+
* previously closed by a needle is taken back.
|
|
14438
|
+
*
|
|
14439
|
+
* ONE row is deliberately still open: an observed KEY the source does not carry
|
|
14440
|
+
* keeps the plaintext when no needle names it. Refusing there needs evidence
|
|
14441
|
+
* that does not exist — the source has NO leaf at that position, so the walk
|
|
14442
|
+
* would be guessing — and the cost of guessing is not bounded: a write-only
|
|
14443
|
+
* credential AWS never echoes back (RDS `MasterUserPassword` and every
|
|
14444
|
+
* `getDriftUnknownPaths` sibling) leaves a reference-bearing source key
|
|
14445
|
+
* unpaired on EVERY readback, so keying the refusal on that would mask
|
|
14446
|
+
* `Runtime` / `FunctionArn` / `LastModified` for every secret-bearing resource
|
|
14447
|
+
* in the account. The extra-KEY asymmetry stated further down is the same
|
|
14448
|
+
* argument; issue [#2868](https://github.com/go-to-k/cdkd/issues/2868) owns the
|
|
14449
|
+
* shape where the plaintext has no counterpart in the source at all.
|
|
14450
|
+
*
|
|
14212
14451
|
* The last row is the price of the row above it and is tracked as issue
|
|
14213
14452
|
* [#2036](https://github.com/go-to-k/cdkd/issues/2036): with no map nothing was
|
|
14214
14453
|
* resolved, so nothing distinguishes a public parameter from a `SecureString`
|
|
@@ -14242,7 +14481,7 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14242
14481
|
* {@link anchorsCorroboratePairing} answers only one of them and its own doc
|
|
14243
14482
|
* says nothing in it is sufficient alone.
|
|
14244
14483
|
*
|
|
14245
|
-
* The
|
|
14484
|
+
* The MASK rows are one root cause, not several: no needle and no
|
|
14246
14485
|
* position, so nothing distinguishes a resolved secret from an ordinary
|
|
14247
14486
|
* literal. They are NOT closed by taking the source subtree, which an earlier
|
|
14248
14487
|
* revision did and the issue #1915 fences correctly rejected — measured, it
|
|
@@ -14257,13 +14496,24 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14257
14496
|
* The MIXED row is the shape this module itself calls DOMINANT for CDK — an
|
|
14258
14497
|
* `Fn::Join` around `secret.secretValueFromJson(...)`.
|
|
14259
14498
|
*
|
|
14260
|
-
* TAKE SOURCE rather than a {@link SECRET_MASK} on the rows it
|
|
14499
|
+
* TAKE SOURCE rather than a {@link SECRET_MASK} on the rows it CERTIFIES, for
|
|
14261
14500
|
* the same reason the whole-token arm does: a mask is not a value `cdkd drift`
|
|
14262
14501
|
* can re-resolve, so it would report a permanent phantom — and `cdkd drift
|
|
14263
14502
|
* --revert` pushes the BASELINE to AWS, so a masked baseline would write the
|
|
14264
14503
|
* literal `***` onto the live resource (the issue #1498 / #1501 class).
|
|
14265
14504
|
*
|
|
14266
|
-
*
|
|
14505
|
+
* That is an argument about a row where a SOURCE VALUE IS AVAILABLE, and it
|
|
14506
|
+
* decides nothing about a row where none is (issue #2852). There the choice is
|
|
14507
|
+
* not mask-versus-source but mask-versus-PLAINTEXT, and the two costs above are
|
|
14508
|
+
* both real: `runAccept` refuses a masked change and
|
|
14509
|
+
* `preserveLiveValuesAtMaskedLeaves` moves AWS's own value in before `--revert`
|
|
14510
|
+
* sends anything, so the mask degrades those two commands on that resource
|
|
14511
|
+
* rather than corrupting it — while the plaintext it replaces is the disclosure
|
|
14512
|
+
* of GHSA-p5qg-v9gv-hc7w sitting in `state.json`. Do not read the paragraph
|
|
14513
|
+
* above as a rule against the mask everywhere; it is a rule about the rows with
|
|
14514
|
+
* a certified source.
|
|
14515
|
+
*
|
|
14516
|
+
* The needle rows were the RESIDUAL and are closed by DERIVED NEEDLES (issue
|
|
14267
14517
|
* #2012) — see {@link deriveReadbackNeedles}. Neither has a position to argue
|
|
14268
14518
|
* from: an unpaired array element and an observed KEY the source does not carry
|
|
14269
14519
|
* are both positions with no source leaf to take. What they never lacked was a
|
|
@@ -14289,7 +14539,7 @@ function deriveReadbackNeedles(bag, source, secrets, rules) {
|
|
|
14289
14539
|
* drift, and a needle learned from a MIS-paired position is a false redaction
|
|
14290
14540
|
* everywhere it then matches.
|
|
14291
14541
|
*/
|
|
14292
|
-
function refuseUncertifiedReadbackPositions(bag, source, secrets, learn, mark) {
|
|
14542
|
+
function refuseUncertifiedReadbackPositions(bag, source, secrets, failClosed, learn, mark) {
|
|
14293
14543
|
if (isDynamicReferenceString(source) && typeof bag === "string") {
|
|
14294
14544
|
if (isSingleDynamicReferenceToken(source)) {
|
|
14295
14545
|
if (learn) learnWholeTokenNeedle(learn, bag, source);
|
|
@@ -14300,25 +14550,26 @@ function refuseUncertifiedReadbackPositions(bag, source, secrets, learn, mark) {
|
|
|
14300
14550
|
return mark ? POSITION_DECIDED : source;
|
|
14301
14551
|
}
|
|
14302
14552
|
if (!subtreeHasDynamicReference(source)) return bag;
|
|
14303
|
-
if (isPlainObject$2(bag) && isPlainObject$2(source)) {
|
|
14553
|
+
if (isPlainObject$2(bag) && hasPlainPrototype(bag) && isPlainObject$2(source)) {
|
|
14304
14554
|
const out = Object.create(null);
|
|
14305
|
-
for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? refuseUncertifiedReadbackPositions(v, source[k], secrets, learn, mark) : v;
|
|
14555
|
+
for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? refuseUncertifiedReadbackPositions(v, source[k], secrets, failClosed, learn, mark) : v;
|
|
14306
14556
|
return out;
|
|
14307
14557
|
}
|
|
14308
14558
|
if (Array.isArray(bag) && Array.isArray(source)) {
|
|
14309
14559
|
const key = identityKeyFor(bag, source);
|
|
14310
14560
|
if (key === void 0) {
|
|
14311
|
-
if (!unkeyedArrayPairsByAnchors(bag, source)) return bag;
|
|
14312
|
-
return bag.map((item, i) => refuseUncertifiedReadbackPositions(item, source[i], secrets, learn, mark));
|
|
14561
|
+
if (!unkeyedArrayPairsByAnchors(bag, source)) return refuseAgainstSource(bag, source, failClosed, mark);
|
|
14562
|
+
return bag.map((item, i) => refuseUncertifiedReadbackPositions(item, source[i], secrets, failClosed, learn, mark));
|
|
14313
14563
|
}
|
|
14314
14564
|
const sourceByIdentity = /* @__PURE__ */ new Map();
|
|
14315
14565
|
for (const item of source) sourceByIdentity.set(item[key], item);
|
|
14566
|
+
const orphanLiterals = unpairedSourceCarriesReference(source, key, new Set(bag.map((item) => item[key]))) && failClosed === true ? wholeStringLeavesOf(source) : void 0;
|
|
14316
14567
|
return bag.map((item) => {
|
|
14317
14568
|
const partner = sourceByIdentity.get(item[key]);
|
|
14318
|
-
return partner === void 0 ? item : refuseUncertifiedReadbackPositions(item, partner, secrets, learn, mark);
|
|
14569
|
+
return partner === void 0 ? orphanLiterals !== void 0 ? refuseUncertifiedSubtree(item, orphanLiterals, mark) : item : refuseUncertifiedReadbackPositions(item, partner, secrets, failClosed, learn, mark);
|
|
14319
14570
|
});
|
|
14320
14571
|
}
|
|
14321
|
-
return bag;
|
|
14572
|
+
return refuseAgainstSource(bag, source, failClosed, mark);
|
|
14322
14573
|
}
|
|
14323
14574
|
/**
|
|
14324
14575
|
* Deep-clone `bag`, replacing every occurrence of a recorded secret value with
|
|
@@ -14334,10 +14585,11 @@ function redactSecretsForState(bag, secrets, source, rules = TEMPLATE_DERIVED_RU
|
|
|
14334
14585
|
if (source !== void 0) {
|
|
14335
14586
|
const positioned = redactByPath(bag, source, secrets, rules, recordedExpressionsOf(secrets), isSameGenerationBag(bag));
|
|
14336
14587
|
if (!isReadbackProjectedFromState(rules)) return positioned;
|
|
14337
|
-
const
|
|
14588
|
+
const failClosed = rules.failClosedOnUncertifiedPositions === true;
|
|
14589
|
+
const refused = refuseUncertifiedReadbackPositions(positioned, source, secrets, failClosed);
|
|
14338
14590
|
const derived = deriveReadbackNeedles(bag, source, secrets, rules);
|
|
14339
14591
|
if (derived === void 0) return refused;
|
|
14340
|
-
const marks = refuseUncertifiedReadbackPositions(positioned, source, secrets, void 0, true);
|
|
14592
|
+
const marks = refuseUncertifiedReadbackPositions(positioned, source, secrets, failClosed, void 0, true);
|
|
14341
14593
|
return preferPositionDecisions(redactSecretsForState(bag, derived.certain), refused, bag, marks, derived.inferred);
|
|
14342
14594
|
}
|
|
14343
14595
|
const regex = buildNeedleRegex(substringNeedlesOf(secrets));
|
|
@@ -14471,7 +14723,7 @@ function scrubResourceRecord(record, secrets, sourceProperties, observedRules) {
|
|
|
14471
14723
|
const next = { ...record };
|
|
14472
14724
|
next.properties = redactSecretsForState(record.properties, secrets, sourceProperties);
|
|
14473
14725
|
if (record.attributes) next.attributes = redactSecretsForState(record.attributes, secrets);
|
|
14474
|
-
if (record.observedProperties) next.observedProperties = redactSecretsForState(record.observedProperties, secrets, sourceProperties ?? next.properties, observedRules ?? (sourceProperties
|
|
14726
|
+
if (record.observedProperties) next.observedProperties = redactSecretsForState(record.observedProperties, secrets, sourceProperties ?? next.properties, observedRules ?? (sourceProperties !== void 0 ? TEMPLATE_SOURCED_RULES : secrets.size === 0 ? STATE_SOURCED_BASELINE_RULES : STATE_SOURCED_READBACK_RULES));
|
|
14475
14727
|
return next;
|
|
14476
14728
|
}
|
|
14477
14729
|
/**
|
|
@@ -19910,6 +20162,113 @@ function s3BucketWebsiteUrl(bucketName, region) {
|
|
|
19910
20162
|
return `http://${bucketName}.s3-website${S3_WEBSITE_ENDPOINT_LEGACY_DASH_REGIONS.has(folded) ? "-" : "."}${folded}.${urlSuffix}`;
|
|
19911
20163
|
}
|
|
19912
20164
|
|
|
20165
|
+
//#endregion
|
|
20166
|
+
//#region src/deployment/drain-budget.ts
|
|
20167
|
+
/**
|
|
20168
|
+
* The drain BUDGET for one intrinsic resolution (issue
|
|
20169
|
+
* [#2563](https://github.com/go-to-k/cdkd/issues/2563)).
|
|
20170
|
+
*
|
|
20171
|
+
* Its own module rather than a corner of `intrinsic-function-resolver.ts`,
|
|
20172
|
+
* and that is forced rather than tidy: 70+ unit files `vi.mock` the resolver
|
|
20173
|
+
* module, and the ones that REPLACE its exports rather than spreading
|
|
20174
|
+
* `...actual` see no new export -- so an opener declared there, whether as a
|
|
20175
|
+
* resolver method or as a module-level function, reddens most of
|
|
20176
|
+
* `tests/unit/deployment` the moment `deploy-engine.ts` imports it (measured,
|
|
20177
|
+
* both ways). A module the engine and the resolver both import, and nobody
|
|
20178
|
+
* mocks, is the shape that works.
|
|
20179
|
+
*/
|
|
20180
|
+
/**
|
|
20181
|
+
* Named `drainDeadlines` from when it held one, and kept because the name is
|
|
20182
|
+
* cited from several comments and a rename is churn this issue does not need.
|
|
20183
|
+
* It holds a {@link DrainBudget} -- remaining WAIT, not an instant.
|
|
20184
|
+
*/
|
|
20185
|
+
const drainDeadlines = new AsyncLocalStorage();
|
|
20186
|
+
/**
|
|
20187
|
+
* Run `fn` under ONE drain budget (issue #2563), inheriting the caller's if
|
|
20188
|
+
* there is one.
|
|
20189
|
+
*
|
|
20190
|
+
* A module-level function rather than a resolver METHOD, deliberately: the
|
|
20191
|
+
* engine's callers mock the resolver wholesale, and a new method on that
|
|
20192
|
+
* surface would make every such mock throw before it resolved anything
|
|
20193
|
+
* (measured -- most of `tests/unit/deployment` red).
|
|
20194
|
+
*
|
|
20195
|
+
* Why a caller needs this. `resolve` opens a budget per CALL, and a caller
|
|
20196
|
+
* that resolves in a LOOP therefore gets one cap per iteration: the outputs
|
|
20197
|
+
* pass walks `template.Outputs` sequentially, so the DRAIN WAIT it could
|
|
20198
|
+
* spend before `saveState`, with the S3 lock held, was `#outputs x` the cap
|
|
20199
|
+
* rather than the cap. CloudFormation allows 200 outputs. Wrapping the loop
|
|
20200
|
+
* here bounds the total drain WAIT under it at one budget.
|
|
20201
|
+
*
|
|
20202
|
+
* The budget is REMAINING milliseconds of wait, not a deadline, and that
|
|
20203
|
+
* distinction is the whole of it: an absolute deadline is spent by wall
|
|
20204
|
+
* clock, so ordinary resolution time between drains burns it although
|
|
20205
|
+
* nothing drained -- an output failing with a 5 ms sibling would leave a
|
|
20206
|
+
* later one with zero grace after 60 s of clean AWS work. Charged only while
|
|
20207
|
+
* a drain is actually waiting, and only once for nested drains whose waits
|
|
20208
|
+
* overlap, an iteration that spent nothing keeps its full grace.
|
|
20209
|
+
*
|
|
20210
|
+
* WHAT IT DOES NOT BOUND, since the cap is not a deadline on the pass: the
|
|
20211
|
+
* cap is armed by a REJECTION and bounds only the extra wait a drain takes
|
|
20212
|
+
* after one. Ordinary resolution time is outside it entirely -- a lookup
|
|
20213
|
+
* that hangs with no sibling rejection anywhere is unbounded here exactly
|
|
20214
|
+
* as it was before this issue, and `withResourceDeadline` does not reach
|
|
20215
|
+
* the outputs pass. So this wrap bounds aggregate drain grace, not the
|
|
20216
|
+
* hold.
|
|
20217
|
+
*
|
|
20218
|
+
* THE TRADE, stated because it is real: iterations that actually WAIT spend
|
|
20219
|
+
* the shared budget, so a later one can find it exhausted and its drain get
|
|
20220
|
+
* no grace. That is the same exposure issue
|
|
20221
|
+
* [#2814](https://github.com/go-to-k/cdkd/issues/2814) records for nesting,
|
|
20222
|
+
* now reachable across a loop as well -- and on the `Export.Name` leg the
|
|
20223
|
+
* cost is a DROPPED write rather than a late one, since that block's
|
|
20224
|
+
* `nameSecrets` is a per-iteration local. What bounds it is that only real
|
|
20225
|
+
* waiting spends the budget: reaching zero takes a full cap of drain wait
|
|
20226
|
+
* inside one pass, not merely a slow pass. It is taken deliberately -- an
|
|
20227
|
+
* unbounded hold on a deploy's state save costs a first deploy every
|
|
20228
|
+
* resource it just created -- and the caller chooses, since a loop that
|
|
20229
|
+
* would rather buy grace per iteration simply does not wrap.
|
|
20230
|
+
*
|
|
20231
|
+
* `evaluateConditions` deliberately does NOT wrap its loop: a failed
|
|
20232
|
+
* condition is downgraded and evaluation continues, it runs before any
|
|
20233
|
+
* resource is provisioned, and one condition's slow parts should
|
|
20234
|
+
* not spend the next one's budget. Its aggregate is `#conditions x` the cap,
|
|
20235
|
+
* with the deploy lock already held -- less severe than the outputs pass,
|
|
20236
|
+
* where the state save is the thing waiting, but not lock-free.
|
|
20237
|
+
*
|
|
20238
|
+
* The other resolve LOOPS in the tree, assessed rather than assumed -- and
|
|
20239
|
+
* the first version of this note got two of them wrong, so each is stated
|
|
20240
|
+
* with what was checked:
|
|
20241
|
+
*
|
|
20242
|
+
* - `cdkd scrub` (`scrub.ts`): lock acquired above, `saveState` downstream,
|
|
20243
|
+
* and its resolve loops -- the resources loop, two output loops, and
|
|
20244
|
+
* `resolveCrossStackReads`, a per-leaf loop the other three each invoke.
|
|
20245
|
+
* One budget is hoisted over all of them. That wrap is INLINE rather than
|
|
20246
|
+
* around a callee, so the callee-keyed table in
|
|
20247
|
+
* `intrinsic-resolver-concurrent-drain.test.ts` cannot see it; an
|
|
20248
|
+
* owner-keyed case in that same file fences it instead.
|
|
20249
|
+
* - `cdkd import` (`import.ts`): `acquireLock` at the root and per child,
|
|
20250
|
+
* `resolveImportedProperties` loops the resources, `saveState` follows.
|
|
20251
|
+
* WRAPPED at both call sites.
|
|
20252
|
+
* - `cdkd export` (`export.ts`): child locks acquired before
|
|
20253
|
+
* `buildResolvedParametersPerStack`, which loops `resolve` over the
|
|
20254
|
+
* intrinsic parameters. WRAPPED.
|
|
20255
|
+
* - `diff-recursive.ts`: loops `resolve` over a parent's parameters with no
|
|
20256
|
+
* lock and no state write -- a read-only diff. LEFT per-call, since a
|
|
20257
|
+
* shared budget would buy no safety and would spend one resolution's
|
|
20258
|
+
* grace on the next.
|
|
20259
|
+
*
|
|
20260
|
+
* The drain is what issue #2563 adds, so the wait in all four is new and
|
|
20261
|
+
* bounding it where it can hold something is part of adding it.
|
|
20262
|
+
*/
|
|
20263
|
+
async function withSharedDrainBudget(fn) {
|
|
20264
|
+
if (drainDeadlines.getStore() !== void 0) return await fn();
|
|
20265
|
+
return await drainDeadlines.run({
|
|
20266
|
+
remaining: void 0,
|
|
20267
|
+
waiting: 0,
|
|
20268
|
+
since: void 0
|
|
20269
|
+
}, fn);
|
|
20270
|
+
}
|
|
20271
|
+
|
|
19913
20272
|
//#endregion
|
|
19914
20273
|
//#region src/deployment/secret-region-classification.ts
|
|
19915
20274
|
/**
|
|
@@ -22133,6 +22492,170 @@ const MAX_LISTED_AVAILABLE_OUTPUTS = 10;
|
|
|
22133
22492
|
*/
|
|
22134
22493
|
const dynamicReferenceRetryDelays = {};
|
|
22135
22494
|
/**
|
|
22495
|
+
* How long {@link allSettledKeepingFirstRejection} waits for the remaining
|
|
22496
|
+
* parts AFTER a rejection is in hand. Double the largest FIXED wait in this
|
|
22497
|
+
* file (the `Fn::GetAtt` `Ipv6CidrBlocks` poll's sleep budget, 15 attempts
|
|
22498
|
+
* x 2 s), which makes it a hang guard rather than a schedule — not a
|
|
22499
|
+
* guarantee that healthy work fits inside it; see the function's own note.
|
|
22500
|
+
*
|
|
22501
|
+
* It is the budget for one CALL of {@link IntrinsicFunctionResolver.resolve}
|
|
22502
|
+
* and everything nested under it: nested drains share the REMAINING wait, so
|
|
22503
|
+
* a template's nesting depth cannot multiply it. It is NOT a bound on a caller
|
|
22504
|
+
* that resolves in a LOOP -- each iteration opens its own budget unless the
|
|
22505
|
+
* caller wraps the loop in {@link withSharedDrainBudget}, which the outputs
|
|
22506
|
+
* pass does and `evaluateConditions` deliberately does not (its aggregate is
|
|
22507
|
+
* `#conditions x` this, before any resource is provisioned but with the
|
|
22508
|
+
* deploy lock already held).
|
|
22509
|
+
*/
|
|
22510
|
+
const DRAIN_AFTER_REJECTION_MS = 6e4;
|
|
22511
|
+
/** Sentinel for "the cap expired", distinguishable from any resolved value. */
|
|
22512
|
+
const CAP_EXPIRED = Symbol("drain-cap-expired");
|
|
22513
|
+
/**
|
|
22514
|
+
* Test seam: overriding `ms` lets a unit test drive the cap without a real
|
|
22515
|
+
* minute of waiting (mirrors {@link dynamicReferenceRetryDelays}).
|
|
22516
|
+
*/
|
|
22517
|
+
const concurrentDrainCap = {};
|
|
22518
|
+
/**
|
|
22519
|
+
* `Promise.all`'s RESULT and its choice of error, with `Promise.allSettled`'s
|
|
22520
|
+
* TIMING: every promise started here has settled before this returns, and
|
|
22521
|
+
* before it throws unless the cap below expires first (issue
|
|
22522
|
+
* [#2563](https://github.com/go-to-k/cdkd/issues/2563)).
|
|
22523
|
+
*
|
|
22524
|
+
* Why the resolver needs that. Resolving a secret dynamic reference RECORDS
|
|
22525
|
+
* `plaintext -> expression` into `context.recordedSecretValues` just before
|
|
22526
|
+
* its promise settles, and that map is what every masking and redaction site
|
|
22527
|
+
* downstream uses as its needle set. Under a bare `Promise.all` a rejecting
|
|
22528
|
+
* part surfaces IMMEDIATELY, so a caller's `catch` / `finally` can run while a
|
|
22529
|
+
* sibling part is still in flight: `DeployEngine`'s `Export.Name` block copies
|
|
22530
|
+
* its private map into the pass map in exactly such a `finally`, and the
|
|
22531
|
+
* sibling's recording then lands in the private map after the copy and reaches
|
|
22532
|
+
* nothing. Draining here fixes it for every caller at once, which a
|
|
22533
|
+
* consumer-side drain cannot — `cdkd scrub`'s shared-map view (issue
|
|
22534
|
+
* [#2531](https://github.com/go-to-k/cdkd/issues/2531)) lets a late write land
|
|
22535
|
+
* whenever it happens but still cannot make it land before the next consumer
|
|
22536
|
+
* runs.
|
|
22537
|
+
*
|
|
22538
|
+
* THE ERROR IS SELECTED BY TIME, NOT BY INPUT ORDER, which is what `Promise.all`
|
|
22539
|
+
* does and what a naive `Promise.allSettled` + "first rejected entry" would
|
|
22540
|
+
* silently change: with two parts rejecting out of input order, the entry scan
|
|
22541
|
+
* reports the LATER one. Each promise gets its own `catch`, so the callbacks
|
|
22542
|
+
* fire in rejection order and the first assignment wins.
|
|
22543
|
+
*
|
|
22544
|
+
* Every input is `catch`-ed, so nothing here can raise an unhandled rejection
|
|
22545
|
+
* while the drain waits.
|
|
22546
|
+
*
|
|
22547
|
+
* THE WAIT IS CAPPED ONCE A REJECTION IS IN HAND. The drain exists to let a
|
|
22548
|
+
* sibling finish RECORDING, and that is worth a wait — but `resolveOutputs`
|
|
22549
|
+
* runs at `deploy-engine.ts`'s worst moment: after every resource has been
|
|
22550
|
+
* created in AWS, before the final `saveState`, with the S3 lock held and its
|
|
22551
|
+
* heartbeat pushing `expiresAt` forward. An unbounded wait there costs a deploy
|
|
22552
|
+
* its state and its lock, and on a FIRST deploy (`currentEtag` undefined, so
|
|
22553
|
+
* the incremental saves were no-ops) every created resource becomes invisible
|
|
22554
|
+
* to cdkd. Nothing else bounds it: `withRetry` caps ATTEMPTS not duration, no
|
|
22555
|
+
* `requestTimeout` is configured, and `withResourceDeadline` wraps
|
|
22556
|
+
* `provisionResourceBody` — which does bound the resource path, property
|
|
22557
|
+
* resolution included, but not the outputs pass. So once a rejection is
|
|
22558
|
+
* recorded the remaining settles race {@link DRAIN_AFTER_REJECTION_MS}, and the
|
|
22559
|
+
* recorded rejection is thrown when it expires.
|
|
22560
|
+
*
|
|
22561
|
+
* The cap is a HANG GUARD, and it does not claim to be more. It is sized
|
|
22562
|
+
* against the largest fixed wait in this file — the `Fn::GetAtt`
|
|
22563
|
+
* `Ipv6CidrBlocks` poll's sleep budget, 15 attempts x 2 s, about 30 s — but
|
|
22564
|
+
* that budget is a floor, not a ceiling: the poll also awaits 15 AWS calls,
|
|
22565
|
+
* and one part can drive several lookups in sequence through object
|
|
22566
|
+
* properties or `Fn::Sub` variables. Two healthy shapes measured on review
|
|
22567
|
+
* already exceed 60 s — three sequential `Ipv6CidrBlocks` polls in one part
|
|
22568
|
+
* is 3 x (15 x 2 s) = 90 s with no hang and no throttling, and five throttled
|
|
22569
|
+
* dynamic references is 5 x (1+2+4+8 s) = 75 s at
|
|
22570
|
+
* `MAX_DYNAMIC_REFERENCE_THROTTLE_RETRIES` = 4. Healthy work CAN therefore
|
|
22571
|
+
* outlast the cap, and when it does its recording lands after the rejection
|
|
22572
|
+
* was released, which is the window this whole function exists to close.
|
|
22573
|
+
* Nothing here cancels that sibling — it keeps running and can record
|
|
22574
|
+
* arbitrarily later — so what the cap bounds is the WAIT, not the lateness.
|
|
22575
|
+
* That is the trade taken deliberately: a bounded wait with a late record
|
|
22576
|
+
* still possible beyond it, against an unbounded hold on a deploy's state
|
|
22577
|
+
* save.
|
|
22578
|
+
*
|
|
22579
|
+
* AND A DRAIN CAN GET NO GRACE AT ALL. The budget is shared REMAINING wait,
|
|
22580
|
+
* so a drain that arms once it is spent gets
|
|
22581
|
+
* `Math.max(0, remaining - openWindow)` = 0 and releases its rejection on the
|
|
22582
|
+
* next macrotask. That applies to any drain NOT ALREADY ARMED when a rejection
|
|
22583
|
+
* reaches it: the ordinary case is an inner drain that spends the full budget
|
|
22584
|
+
* and throws, whose every not-yet-armed ancestor then arms against an
|
|
22585
|
+
* exhausted remaining-wait budget. What it does NOT mean is that a fast sibling is exposed: a
|
|
22586
|
+
* sibling still pending at that moment has itself been running at least the
|
|
22587
|
+
* budget. What it means is that the sibling's own RECORDING gets no wait --
|
|
22588
|
+
* a lookup begun late inside a long-running part is fast in itself and still
|
|
22589
|
+
* lands after the rejection was released. That reasoning covers the NESTED
|
|
22590
|
+
* ancestor case; a caller that wraps a LOOP widens it, because a later
|
|
22591
|
+
* iteration starts FRESH siblings against a budget an earlier one already
|
|
22592
|
+
* spent and those need not be long-running at all. So the exposure does not
|
|
22593
|
+
* require the "healthy work slower than 60 s" shape the sizing paragraph
|
|
22594
|
+
* describes; that shape is the cheapest way to reach it with no nesting and
|
|
22595
|
+
* no wrapped loop, not the floor.
|
|
22596
|
+
* Residual: issue
|
|
22597
|
+
* [#2814](https://github.com/go-to-k/cdkd/issues/2814).
|
|
22598
|
+
*
|
|
22599
|
+
* ONE REMAINING CONSEQUENCE, deliberate and pinned by a case rather than only
|
|
22600
|
+
* described: the earliest-in-time rule holds PER INVOCATION, not across
|
|
22601
|
+
* NESTED resolutions. For a join whose parts are `[listWithAnEarlyFailure,
|
|
22602
|
+
* laterFailure]`, the inner list's drain holds its own rejection while its
|
|
22603
|
+
* slow sibling finishes, so the outer join captures the later failure first
|
|
22604
|
+
* and reports that instead. That is not only cosmetic: the retry classifiers
|
|
22605
|
+
* DO read the message (`retryClassificationText` feeds
|
|
22606
|
+
* `isRetryableTransientError`, whose `RETRYABLE_ERROR_MESSAGE_PATTERNS` is an
|
|
22607
|
+
* explicit substring table), so swapping which failure surfaces can swap a
|
|
22608
|
+
* transient verdict for a terminal one. What does not change is that the
|
|
22609
|
+
* resolution FAILS: both are genuine failures of the same resolve, and the
|
|
22610
|
+
* selection was already timing-dependent — `Promise.all` reports whichever
|
|
22611
|
+
* lost the race. The drain adds a systematic bias toward the SHALLOWER
|
|
22612
|
+
* failure where the old race was arbitrary. Residual: issue
|
|
22613
|
+
* [#2805](https://github.com/go-to-k/cdkd/issues/2805).
|
|
22614
|
+
*/
|
|
22615
|
+
async function allSettledKeepingFirstRejection(promises) {
|
|
22616
|
+
let rejection;
|
|
22617
|
+
const shared = drainDeadlines.getStore();
|
|
22618
|
+
let armCap;
|
|
22619
|
+
const guarded = promises.map((promise) => promise.catch((error) => {
|
|
22620
|
+
if (rejection === void 0) {
|
|
22621
|
+
rejection = { error };
|
|
22622
|
+
armCap?.();
|
|
22623
|
+
}
|
|
22624
|
+
}));
|
|
22625
|
+
let capTimer;
|
|
22626
|
+
let charged = false;
|
|
22627
|
+
const capped = new Promise((resolve) => {
|
|
22628
|
+
armCap = () => {
|
|
22629
|
+
const budget = concurrentDrainCap.ms ?? DRAIN_AFTER_REJECTION_MS;
|
|
22630
|
+
let wait = budget;
|
|
22631
|
+
if (shared !== void 0) {
|
|
22632
|
+
shared.remaining ??= budget;
|
|
22633
|
+
const openWindow = shared.since === void 0 ? 0 : Date.now() - shared.since;
|
|
22634
|
+
wait = Math.max(0, shared.remaining - openWindow);
|
|
22635
|
+
if (shared.waiting === 0) shared.since = Date.now();
|
|
22636
|
+
shared.waiting += 1;
|
|
22637
|
+
charged = true;
|
|
22638
|
+
}
|
|
22639
|
+
capTimer = setTimeout(() => resolve(CAP_EXPIRED), wait);
|
|
22640
|
+
};
|
|
22641
|
+
});
|
|
22642
|
+
try {
|
|
22643
|
+
const outcome = await Promise.race([Promise.all(guarded), capped]);
|
|
22644
|
+
if (rejection !== void 0) throw rejection.error;
|
|
22645
|
+
if (outcome === CAP_EXPIRED) throw markNonRetryable(/* @__PURE__ */ new Error("drain cap expired with no rejection recorded"));
|
|
22646
|
+
return outcome;
|
|
22647
|
+
} finally {
|
|
22648
|
+
if (capTimer !== void 0) clearTimeout(capTimer);
|
|
22649
|
+
if (charged && shared !== void 0) {
|
|
22650
|
+
shared.waiting -= 1;
|
|
22651
|
+
if (shared.waiting === 0 && shared.since !== void 0) {
|
|
22652
|
+
shared.remaining = Math.max(0, (shared.remaining ?? 0) - (Date.now() - shared.since));
|
|
22653
|
+
shared.since = void 0;
|
|
22654
|
+
}
|
|
22655
|
+
}
|
|
22656
|
+
}
|
|
22657
|
+
}
|
|
22658
|
+
/**
|
|
22136
22659
|
* Is `region` safe to build an AWS SDK client from?
|
|
22137
22660
|
*
|
|
22138
22661
|
* This is a SECURITY gate, not an AWS region registry, and the distinction
|
|
@@ -23035,7 +23558,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23035
23558
|
* Resolve all intrinsic functions in a value
|
|
23036
23559
|
*/
|
|
23037
23560
|
async resolve(value, context) {
|
|
23038
|
-
return await this.resolveValue(value, context);
|
|
23561
|
+
return await withSharedDrainBudget(() => this.resolveValue(value, context));
|
|
23039
23562
|
}
|
|
23040
23563
|
/**
|
|
23041
23564
|
* Evaluate all conditions in the template
|
|
@@ -23077,7 +23600,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23077
23600
|
}
|
|
23078
23601
|
};
|
|
23079
23602
|
for (const name of Object.keys(templateConditions)) try {
|
|
23080
|
-
await evaluateByName(name);
|
|
23603
|
+
await withSharedDrainBudget(() => evaluateByName(name));
|
|
23081
23604
|
} catch (error) {
|
|
23082
23605
|
this.logger.warn(this.maskSecretsForLog(`Failed to evaluate condition ${name}: ${error instanceof Error ? error.message : String(error)}, assuming false`, maskingContext));
|
|
23083
23606
|
conditions[name] = false;
|
|
@@ -23093,7 +23616,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23093
23616
|
if (typeof value === "string" && value.includes("{{resolve:")) return await this.resolveDynamicReferences(value, context);
|
|
23094
23617
|
return value;
|
|
23095
23618
|
}
|
|
23096
|
-
if (Array.isArray(value)) return (await
|
|
23619
|
+
if (Array.isArray(value)) return (await allSettledKeepingFirstRejection(value.map((v) => this.resolveValue(v, context)))).filter((v) => v !== AWS_NO_VALUE);
|
|
23097
23620
|
const obj = value;
|
|
23098
23621
|
if ("Ref" in obj) return await this.resolveRef(obj["Ref"], context);
|
|
23099
23622
|
if ("Fn::GetAtt" in obj) return await this.resolveGetAtt(obj["Fn::GetAtt"], context);
|
|
@@ -23397,7 +23920,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23397
23920
|
const [rawLogicalId, rawAttributeName] = getAtt;
|
|
23398
23921
|
logicalId = rawLogicalId;
|
|
23399
23922
|
const resolvedAttributeName = await this.resolveValue(rawAttributeName, context);
|
|
23400
|
-
if (typeof resolvedAttributeName !== "string") throw new Error(`Fn::GetAtt attribute name for ${logicalId} must resolve to a string, got ${typeof resolvedAttributeName}: ${stringifyValue(resolvedAttributeName)}`);
|
|
23923
|
+
if (typeof resolvedAttributeName !== "string") throw new Error(`Fn::GetAtt attribute name for ${logicalId} must resolve to a string, got ${typeof resolvedAttributeName}: ${stringifyValue(this.maskValueLeaves(resolvedAttributeName, context))}`);
|
|
23401
23924
|
attributeName = resolvedAttributeName;
|
|
23402
23925
|
} else {
|
|
23403
23926
|
const split = splitGetAttStringForm(getAtt);
|
|
@@ -23410,13 +23933,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23410
23933
|
if (!(resource.resourceType === "AWS::EC2::VPC" && attributeName === "Ipv6CidrBlocks") && resource.attributes !== void 0) {
|
|
23411
23934
|
const flatValue = Object.hasOwn(resource.attributes, attributeName) ? resource.attributes[attributeName] : void 0;
|
|
23412
23935
|
if (flatValue !== void 0) {
|
|
23413
|
-
this.rejectPlaceholderArnAttribute(resource, attributeName, flatValue, logicalId);
|
|
23936
|
+
this.rejectPlaceholderArnAttribute(resource, attributeName, flatValue, logicalId, context);
|
|
23414
23937
|
if (resource.resourceType === "AWS::Route53::HostedZone" && attributeName === "NameServers" && typeof flatValue === "string") {
|
|
23415
23938
|
const nameServers = flatValue === "" ? [] : flatValue.split(",");
|
|
23416
|
-
this.logger.debug(`Normalized legacy Fn::GetAtt attribute: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, nameServers)}`);
|
|
23939
|
+
this.logger.debug(`Normalized legacy Fn::GetAtt attribute: ${logicalId}.${this.maskSecretsForLog(attributeName, context)} -> ${this.maskSecretsForLog(stringifyAttributeForLog(attributeName, nameServers), context)}`);
|
|
23417
23940
|
return this.noteAttributeSecrecy(logicalId, attributeName, nameServers, context);
|
|
23418
23941
|
}
|
|
23419
|
-
this.logger.debug(`Resolved Fn::GetAtt from attributes: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, flatValue)}`);
|
|
23942
|
+
this.logger.debug(`Resolved Fn::GetAtt from attributes: ${logicalId}.${this.maskSecretsForLog(attributeName, context)} -> ${this.maskSecretsForLog(stringifyAttributeForLog(attributeName, flatValue), context)}`);
|
|
23420
23943
|
if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE$1 && attributeName.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX) && carriesDynamicReference(flatValue)) return await this.reresolveCrossStackValue(flatValue, nestedStackChildRegionFromLocalArn(resource.physicalId), context, `nested stack ${logicalId} ${attributeName}`, crossStackSourceKey({ "Fn::GetAtt": getAtt }));
|
|
23421
23944
|
return this.noteAttributeSecrecy(logicalId, attributeName, flatValue, context);
|
|
23422
23945
|
}
|
|
@@ -23429,17 +23952,17 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23429
23952
|
break;
|
|
23430
23953
|
}
|
|
23431
23954
|
if (cursor !== void 0) {
|
|
23432
|
-
this.logger.debug(`Resolved Fn::GetAtt from nested attributes: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, cursor)}`);
|
|
23955
|
+
this.logger.debug(`Resolved Fn::GetAtt from nested attributes: ${logicalId}.${this.maskSecretsForLog(attributeName, context)} -> ${this.maskSecretsForLog(stringifyAttributeForLog(attributeName, cursor), context)}`);
|
|
23433
23956
|
return this.noteAttributeSecrecy(logicalId, attributeName, cursor, context);
|
|
23434
23957
|
}
|
|
23435
23958
|
}
|
|
23436
23959
|
}
|
|
23437
23960
|
if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE$1 && attributeName.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX)) {
|
|
23438
23961
|
const declared = Object.keys(resource.attributes ?? {}).filter((k) => k.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX)).map((k) => k.slice(8)).sort();
|
|
23439
|
-
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${attributeName}]: the nested stack '${logicalId}' declares no output named '${attributeName.slice(8)}'. Its outputs are ${declared.length > 0 ? declared.join(", ") : "(none)"}. Check the output name in the nested stack's template, and deploy the child stack again if you have just added it.`));
|
|
23962
|
+
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}]: the nested stack '${logicalId}' declares no output named '${this.maskSecretsForLog(attributeName.slice(8), context)}'. Its outputs are ${declared.length > 0 ? declared.join(", ") : "(none)"}. Check the output name in the nested stack's template, and deploy the child stack again if you have just added it.`));
|
|
23440
23963
|
}
|
|
23441
23964
|
const value = await this.constructGuardedAttribute(resource, attributeName, context, logicalId);
|
|
23442
|
-
this.logger.debug(`Resolved Fn::GetAtt: ${logicalId}.${attributeName} -> ${stringifyAttributeForLog(attributeName, value)}`);
|
|
23965
|
+
this.logger.debug(`Resolved Fn::GetAtt: ${logicalId}.${this.maskSecretsForLog(attributeName, context)} -> ${this.maskSecretsForLog(stringifyAttributeForLog(attributeName, value), context)}`);
|
|
23443
23966
|
return value;
|
|
23444
23967
|
}
|
|
23445
23968
|
/**
|
|
@@ -23490,7 +24013,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23490
24013
|
const declared = context.noEchoAttributeResources?.get(logicalId);
|
|
23491
24014
|
if ((declared === true || declared !== void 0 && declared.has(attributeName)) && context.recordedSecretValues) recordMaskOnlyValuesIn(value, context.recordedSecretValues);
|
|
23492
24015
|
if (context.redactedAttributeReads !== void 0 && carriesSecretMask(value)) {
|
|
23493
|
-
const read = `${logicalId}.${attributeName}`;
|
|
24016
|
+
const read = `${logicalId}.${this.maskSecretsForLog(attributeName, context)}`;
|
|
23494
24017
|
if (!context.redactedAttributeReads.includes(read)) context.redactedAttributeReads.push(read);
|
|
23495
24018
|
}
|
|
23496
24019
|
return value;
|
|
@@ -23524,10 +24047,10 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23524
24047
|
* honest answer is to say so and name the remedy: the record heals on the
|
|
23525
24048
|
* resource's next in-place update (#1727).
|
|
23526
24049
|
*/
|
|
23527
|
-
rejectPlaceholderArnAttribute(resource, attributeName, value, logicalId) {
|
|
24050
|
+
rejectPlaceholderArnAttribute(resource, attributeName, value, logicalId, context) {
|
|
23528
24051
|
if (!REF_RETURNS_ARN_FROM_STATE.get(resource.resourceType)?.includes(attributeName)) return;
|
|
23529
24052
|
if (typeof value !== "string" || !isPlaceholderArn(value)) return;
|
|
23530
|
-
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${attributeName}] for ${resource.resourceType}: the recorded value "${value}" is a placeholder written by a cdkd version older than issue #1681 — its region and account fields are literal wildcards, so it is not a usable ARN. Deploy the stack again so the resource's next update heals the record (cdkd now records the real ARN), or re-import the resource.`));
|
|
24053
|
+
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resource.resourceType}: the recorded value "${stringifyValue(this.maskValueLeaves(value, context))}" is a placeholder written by a cdkd version older than issue #1681 — its region and account fields are literal wildcards, so it is not a usable ARN. Deploy the stack again so the resource's next update heals the record (cdkd now records the real ARN), or re-import the resource.`));
|
|
23531
24054
|
}
|
|
23532
24055
|
/**
|
|
23533
24056
|
* Construct resource attribute value based on resource type, refusing to
|
|
@@ -23572,7 +24095,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23572
24095
|
async constructGuardedAttribute(resource, attributeName, context, logicalId) {
|
|
23573
24096
|
const accountInfo = await getAccountInfo(this.resolverRegion);
|
|
23574
24097
|
const value = await this.constructAttribute(resource, attributeName, context, logicalId, accountInfo);
|
|
23575
|
-
if (accountInfo.fabricated && embedsAccountId(value, accountInfo.accountId)) throw new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${attributeName}] for ${resource.resourceType}: STS did not report this deploy's account id, so cdkd would build the value from the placeholder account ${accountInfo.accountId} — structurally valid, naming a different account, and indistinguishable downstream from a real one. Fix the AWS credentials (or set AWS_ACCOUNT_ID to this deploy's account) and deploy again.`);
|
|
24098
|
+
if (accountInfo.fabricated && embedsAccountId(value, accountInfo.accountId)) throw new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resource.resourceType}: STS did not report this deploy's account id, so cdkd would build the value from the placeholder account ${accountInfo.accountId} — structurally valid, naming a different account, and indistinguishable downstream from a real one. Fix the AWS credentials (or set AWS_ACCOUNT_ID to this deploy's account) and deploy again.`);
|
|
23576
24099
|
return value;
|
|
23577
24100
|
}
|
|
23578
24101
|
/**
|
|
@@ -23585,14 +24108,14 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23585
24108
|
* result. Keep this method's NAME — `scripts/gen-sdk-attr-coverage.ts` reads
|
|
23586
24109
|
* the resource types it references.
|
|
23587
24110
|
*/
|
|
23588
|
-
async constructAttribute(resource, attributeName,
|
|
24111
|
+
async constructAttribute(resource, attributeName, context, logicalId, accountInfo) {
|
|
23589
24112
|
const { resourceType, physicalId } = resource;
|
|
23590
24113
|
const { accountId, partition } = accountInfo;
|
|
23591
24114
|
const region = canonicalizeRegion(accountInfo.region);
|
|
23592
24115
|
if (resourceType === "AWS::DynamoDB::Table" || resourceType === "AWS::DynamoDB::GlobalTable") switch (attributeName) {
|
|
23593
24116
|
case "Arn": return `arn:${partition}:dynamodb:${region}:${accountId}:table/${physicalId}`;
|
|
23594
24117
|
case "StreamArn": return;
|
|
23595
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24118
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23596
24119
|
}
|
|
23597
24120
|
if (resourceType === "AWS::S3::Bucket") switch (attributeName) {
|
|
23598
24121
|
case "Arn": return s3BucketArn(physicalId, region);
|
|
@@ -23600,12 +24123,12 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23600
24123
|
case "RegionalDomainName": return s3BucketRegionalDomainName(physicalId, region);
|
|
23601
24124
|
case "DualStackDomainName": return s3BucketDualStackDomainName(physicalId, region);
|
|
23602
24125
|
case "WebsiteURL": return s3BucketWebsiteUrl(physicalId, region);
|
|
23603
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24126
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23604
24127
|
}
|
|
23605
24128
|
if (resourceType === "AWS::IAM::Role") switch (attributeName) {
|
|
23606
24129
|
case "Arn": return `arn:${partition}:iam::${accountId}:role/${physicalId}`;
|
|
23607
24130
|
case "RoleId": return;
|
|
23608
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24131
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23609
24132
|
}
|
|
23610
24133
|
if (resourceType === "AWS::EC2::VPC") switch (attributeName) {
|
|
23611
24134
|
case "VpcId": return physicalId;
|
|
@@ -23617,7 +24140,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23617
24140
|
const associations = (await ec2.send(new DescribeVpcsCommand({ VpcIds: [physicalId] }))).Vpcs?.[0]?.Ipv6CidrBlockAssociationSet || [];
|
|
23618
24141
|
const blocks = associations.filter((a) => a.Ipv6CidrBlockState?.State === "associated").map((a) => a.Ipv6CidrBlock);
|
|
23619
24142
|
if (blocks.length > 0) {
|
|
23620
|
-
this.logger.debug(`Resolved VPC Ipv6CidrBlocks for ${physicalId}: ${JSON.stringify(blocks)}`);
|
|
24143
|
+
this.logger.debug(`Resolved VPC Ipv6CidrBlocks for ${physicalId}: ${this.maskSecretsForLog(JSON.stringify(this.maskValueLeaves(blocks, context)), context)}`);
|
|
23621
24144
|
return blocks;
|
|
23622
24145
|
}
|
|
23623
24146
|
if (associations.filter((a) => a.Ipv6CidrBlockState?.State === "associating").length === 0) {
|
|
@@ -23634,38 +24157,38 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23634
24157
|
return [];
|
|
23635
24158
|
}
|
|
23636
24159
|
case "DefaultSecurityGroup": return resource.attributes?.["DefaultSecurityGroup"] || physicalId;
|
|
23637
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24160
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23638
24161
|
}
|
|
23639
24162
|
if (resourceType === "AWS::IAM::Policy") switch (attributeName) {
|
|
23640
24163
|
case "Arn": return `arn:${partition}:iam::${accountId}:policy/${physicalId}`;
|
|
23641
24164
|
case "PolicyId": return;
|
|
23642
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24165
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23643
24166
|
}
|
|
23644
24167
|
if (resourceType === "AWS::IAM::User") switch (attributeName) {
|
|
23645
24168
|
case "Arn": return `arn:${partition}:iam::${accountId}:user/${physicalId}`;
|
|
23646
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24169
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23647
24170
|
}
|
|
23648
24171
|
if (resourceType === "AWS::IAM::Group") switch (attributeName) {
|
|
23649
24172
|
case "Arn": return `arn:${partition}:iam::${accountId}:group/${physicalId}`;
|
|
23650
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24173
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23651
24174
|
}
|
|
23652
24175
|
if (resourceType === "AWS::IAM::InstanceProfile") switch (attributeName) {
|
|
23653
24176
|
case "Arn": return `arn:${partition}:iam::${accountId}:instance-profile/${physicalId}`;
|
|
23654
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24177
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23655
24178
|
}
|
|
23656
24179
|
if (resourceType === "AWS::KMS::Key") switch (attributeName) {
|
|
23657
24180
|
case "Arn": return `arn:${partition}:kms:${region}:${accountId}:key/${physicalId}`;
|
|
23658
24181
|
case "KeyId": return physicalId;
|
|
23659
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24182
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23660
24183
|
}
|
|
23661
24184
|
if (resourceType === "AWS::Cognito::UserPool") switch (attributeName) {
|
|
23662
24185
|
case "Arn": return `arn:${partition}:cognito-idp:${region}:${accountId}:userpool/${physicalId}`;
|
|
23663
24186
|
case "UserPoolId": return physicalId;
|
|
23664
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24187
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23665
24188
|
}
|
|
23666
24189
|
if (resourceType === "AWS::Kinesis::Stream") switch (attributeName) {
|
|
23667
24190
|
case "Arn": return `arn:${partition}:kinesis:${region}:${accountId}:stream/${physicalId}`;
|
|
23668
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24191
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23669
24192
|
}
|
|
23670
24193
|
if (resourceType === "AWS::Events::Rule") switch (attributeName) {
|
|
23671
24194
|
case "Arn": {
|
|
@@ -23675,41 +24198,41 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23675
24198
|
const busName = bus.startsWith("arn:") ? bus.split("/").pop() || "" : bus;
|
|
23676
24199
|
return busName ? `arn:${partition}:events:${region}:${accountId}:rule/${busName}/${physicalId}` : `arn:${partition}:events:${region}:${accountId}:rule/${physicalId}`;
|
|
23677
24200
|
}
|
|
23678
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24201
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23679
24202
|
}
|
|
23680
24203
|
if (resourceType === "AWS::Events::EventBus") switch (attributeName) {
|
|
23681
24204
|
case "Arn": return `arn:${partition}:events:${region}:${accountId}:event-bus/${physicalId}`;
|
|
23682
24205
|
case "Name": return physicalId;
|
|
23683
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24206
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23684
24207
|
}
|
|
23685
24208
|
if (resourceType === "AWS::EFS::FileSystem") switch (attributeName) {
|
|
23686
24209
|
case "Arn": return `arn:${partition}:elasticfilesystem:${region}:${accountId}:file-system/${physicalId}`;
|
|
23687
24210
|
case "FileSystemId": return physicalId;
|
|
23688
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24211
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23689
24212
|
}
|
|
23690
24213
|
if (resourceType === "AWS::KinesisFirehose::DeliveryStream") switch (attributeName) {
|
|
23691
24214
|
case "Arn": return `arn:${partition}:firehose:${region}:${accountId}:deliverystream/${physicalId}`;
|
|
23692
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24215
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23693
24216
|
}
|
|
23694
24217
|
if (resourceType === "AWS::CodeBuild::Project") switch (attributeName) {
|
|
23695
24218
|
case "Arn": return `arn:${partition}:codebuild:${region}:${accountId}:project/${physicalId}`;
|
|
23696
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24219
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23697
24220
|
}
|
|
23698
24221
|
if (resourceType === "AWS::CloudTrail::Trail") switch (attributeName) {
|
|
23699
24222
|
case "Arn":
|
|
23700
24223
|
if (physicalId.startsWith("arn:")) return physicalId;
|
|
23701
24224
|
return `arn:${partition}:cloudtrail:${region}:${accountId}:trail/${physicalId}`;
|
|
23702
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24225
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23703
24226
|
}
|
|
23704
24227
|
if (resourceType === "AWS::AppSync::GraphQLApi") switch (attributeName) {
|
|
23705
24228
|
case "Arn": return `arn:${partition}:appsync:${region}:${accountId}:apis/${physicalId}`;
|
|
23706
24229
|
case "ApiId": return physicalId;
|
|
23707
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24230
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23708
24231
|
}
|
|
23709
24232
|
if (resourceType === "AWS::ApiGatewayV2::Api") switch (attributeName) {
|
|
23710
24233
|
case "ExecuteApiArn": return `arn:${partition}:execute-api:${region}:${accountId}:${physicalId}`;
|
|
23711
24234
|
case "ApiId": return physicalId;
|
|
23712
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24235
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23713
24236
|
}
|
|
23714
24237
|
if (resourceType === "AWS::ServiceDiscovery::PrivateDnsNamespace" || resourceType === "AWS::ServiceDiscovery::HttpNamespace" || resourceType === "AWS::ServiceDiscovery::PublicDnsNamespace") switch (attributeName) {
|
|
23715
24238
|
case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:namespace/${physicalId}`;
|
|
@@ -23721,38 +24244,38 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23721
24244
|
this.logger.warn(`Failed to fetch HostedZoneId for namespace ${physicalId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
23722
24245
|
return;
|
|
23723
24246
|
}
|
|
23724
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24247
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23725
24248
|
}
|
|
23726
24249
|
if (resourceType === "AWS::ServiceDiscovery::Service") switch (attributeName) {
|
|
23727
24250
|
case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:service/${physicalId}`;
|
|
23728
24251
|
case "Id": return physicalId;
|
|
23729
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24252
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23730
24253
|
}
|
|
23731
24254
|
if (resourceType === "AWS::CloudWatch::Alarm") switch (attributeName) {
|
|
23732
24255
|
case "Arn": return `arn:${partition}:cloudwatch:${region}:${accountId}:alarm:${physicalId}`;
|
|
23733
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24256
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23734
24257
|
}
|
|
23735
24258
|
if (resourceType === "AWS::CloudWatch::CompositeAlarm") switch (attributeName) {
|
|
23736
24259
|
case "Arn": return `arn:${partition}:cloudwatch:${region}:${accountId}:alarm:${physicalId}`;
|
|
23737
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24260
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23738
24261
|
}
|
|
23739
24262
|
if (resourceType === "AWS::RDS::DBInstance" || resourceType === "AWS::DocDB::DBInstance" || resourceType === "AWS::Neptune::DBInstance") switch (attributeName) {
|
|
23740
24263
|
case "DBInstanceArn":
|
|
23741
24264
|
case "Arn": return `arn:${partition}:rds:${region}:${accountId}:db:${physicalId}`;
|
|
23742
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24265
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23743
24266
|
}
|
|
23744
24267
|
if (resourceType === "AWS::RDS::DBCluster" || resourceType === "AWS::DocDB::DBCluster" || resourceType === "AWS::Neptune::DBCluster") switch (attributeName) {
|
|
23745
24268
|
case "DBClusterArn":
|
|
23746
24269
|
case "Arn": return `arn:${partition}:rds:${region}:${accountId}:cluster:${physicalId}`;
|
|
23747
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24270
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23748
24271
|
}
|
|
23749
24272
|
if (resourceType === "AWS::S3Express::DirectoryBucket") switch (attributeName) {
|
|
23750
24273
|
case "Arn": return `arn:${partition}:s3express:${region}:${accountId}:bucket/${physicalId}`;
|
|
23751
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24274
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23752
24275
|
}
|
|
23753
24276
|
if (resourceType === "AWS::Lambda::Function") switch (attributeName) {
|
|
23754
24277
|
case "Arn": return `arn:${partition}:lambda:${region}:${accountId}:function:${physicalId}`;
|
|
23755
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24278
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23756
24279
|
}
|
|
23757
24280
|
if (resourceType === "AWS::SQS::Queue") {
|
|
23758
24281
|
let queueName = physicalId;
|
|
@@ -23764,26 +24287,26 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23764
24287
|
case "Arn": return `arn:${partition}:sqs:${region}:${accountId}:${queueName}`;
|
|
23765
24288
|
case "QueueUrl": return physicalId;
|
|
23766
24289
|
case "QueueName": return queueName;
|
|
23767
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24290
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23768
24291
|
}
|
|
23769
24292
|
}
|
|
23770
24293
|
if (resourceType === "AWS::SNS::Topic") switch (attributeName) {
|
|
23771
24294
|
case "TopicArn": return `arn:${partition}:sns:${region}:${accountId}:${physicalId}`;
|
|
23772
24295
|
case "TopicName": return physicalId;
|
|
23773
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24296
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23774
24297
|
}
|
|
23775
24298
|
if (resourceType === "AWS::Logs::LogGroup") switch (attributeName) {
|
|
23776
24299
|
case "Arn": return `arn:${partition}:logs:${region}:${accountId}:log-group:${physicalId}:*`;
|
|
23777
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24300
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23778
24301
|
}
|
|
23779
24302
|
if (resourceType === "AWS::ECR::Repository") switch (attributeName) {
|
|
23780
24303
|
case "Arn": return `arn:${partition}:ecr:${region}:${accountId}:repository/${physicalId}`;
|
|
23781
24304
|
case "RepositoryUri": return `${accountId}.dkr.ecr.${region}.${derivePartitionAndUrlSuffix(region).urlSuffix}/${physicalId}`;
|
|
23782
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24305
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23783
24306
|
}
|
|
23784
24307
|
if (resourceType === "AWS::ECS::Cluster") switch (attributeName) {
|
|
23785
24308
|
case "Arn": return `arn:${partition}:ecs:${region}:${accountId}:cluster/${physicalId}`;
|
|
23786
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24309
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23787
24310
|
}
|
|
23788
24311
|
if (resourceType === "AWS::ECS::Service") switch (attributeName) {
|
|
23789
24312
|
case "Name": {
|
|
@@ -23807,16 +24330,16 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23807
24330
|
const serviceName = physicalId.substring(pipeIdx + 1);
|
|
23808
24331
|
return `${left.substring(0, clusterIdx)}:service/${clusterName}/${serviceName}`;
|
|
23809
24332
|
}
|
|
23810
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24333
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23811
24334
|
}
|
|
23812
24335
|
if (resourceType === "AWS::EC2::SecurityGroup") switch (attributeName) {
|
|
23813
24336
|
case "GroupId": return physicalId;
|
|
23814
24337
|
case "VpcId": return;
|
|
23815
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24338
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23816
24339
|
}
|
|
23817
24340
|
if (resourceType === "AWS::EC2::Subnet") switch (attributeName) {
|
|
23818
24341
|
case "SubnetId": return physicalId;
|
|
23819
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24342
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23820
24343
|
}
|
|
23821
24344
|
if (resourceType === "AWS::EC2::Instance") switch (attributeName) {
|
|
23822
24345
|
case "InstanceId": return physicalId;
|
|
@@ -23850,13 +24373,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23850
24373
|
cachedEc2InstanceAttributes[cacheKey] = value;
|
|
23851
24374
|
return value;
|
|
23852
24375
|
}
|
|
23853
|
-
this.logger.warn(`DescribeInstances(${physicalId}) returned no ${attributeName}; returning physical ID`);
|
|
24376
|
+
this.logger.warn(`DescribeInstances(${physicalId}) returned no ${this.maskSecretsForLog(attributeName, context)}; returning physical ID`);
|
|
23854
24377
|
} catch (err) {
|
|
23855
|
-
this.logger.warn(`DescribeInstances(${physicalId}) failed for ${attributeName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
24378
|
+
this.logger.warn(`DescribeInstances(${physicalId}) failed for ${this.maskSecretsForLog(attributeName, context)}: ${this.maskSecretsForLog(err instanceof Error ? err.message : String(err), context)}`);
|
|
23856
24379
|
}
|
|
23857
24380
|
return physicalId;
|
|
23858
24381
|
}
|
|
23859
|
-
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24382
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23860
24383
|
}
|
|
23861
24384
|
if (resourceType === "AWS::EC2::LaunchTemplate") {
|
|
23862
24385
|
if (attributeName === "LatestVersionNumber" || attributeName === "DefaultVersionNumber") {
|
|
@@ -23865,14 +24388,14 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23865
24388
|
const value = attributeName === "LatestVersionNumber" ? lt?.LatestVersionNumber : lt?.DefaultVersionNumber;
|
|
23866
24389
|
if (value !== void 0 && value !== null) return String(value);
|
|
23867
24390
|
} catch (err) {
|
|
23868
|
-
this.logger.warn(`DescribeLaunchTemplates(${physicalId}) failed for ${attributeName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
24391
|
+
this.logger.warn(`DescribeLaunchTemplates(${physicalId}) failed for ${this.maskSecretsForLog(attributeName, context)}: ${this.maskSecretsForLog(err instanceof Error ? err.message : String(err), context)}`);
|
|
23869
24392
|
}
|
|
23870
24393
|
return attributeName === "LatestVersionNumber" ? "$Latest" : "$Default";
|
|
23871
24394
|
}
|
|
23872
24395
|
if (attributeName === "LaunchTemplateId") return physicalId;
|
|
23873
|
-
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24396
|
+
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23874
24397
|
}
|
|
23875
|
-
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
24398
|
+
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
23876
24399
|
}
|
|
23877
24400
|
/**
|
|
23878
24401
|
* Shared unknown-attribute physicalId fallback (issues #1106 / #1111).
|
|
@@ -23901,13 +24424,16 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23901
24424
|
* route through this helper — those are explicit `case`s in the
|
|
23902
24425
|
* per-type handlers.
|
|
23903
24426
|
*/
|
|
23904
|
-
guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId) {
|
|
24427
|
+
guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context) {
|
|
23905
24428
|
const expectsArnShape = attributeName.endsWith("Arn") && !physicalId.startsWith("arn:");
|
|
23906
24429
|
const expectsUrlShape = attributeName.endsWith("Url") && !/^https?:\/\//.test(physicalId);
|
|
23907
|
-
if (expectsArnShape || expectsUrlShape)
|
|
23908
|
-
|
|
24430
|
+
if (expectsArnShape || expectsUrlShape) {
|
|
24431
|
+
const expectedShape = expectsArnShape ? "an ARN (arn:...)" : "a URL (http(s)://...)";
|
|
24432
|
+
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resourceType}: attributes are not enriched for this resource type, and the physical ID fallback "${this.maskSecretsForLog(physicalId, context)}" is not ${expectedShape}. CloudFormation would return a different value here, so falling back to the physical ID would silently produce a wrong value (e.g. in stack Outputs). Avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${this.maskSecretsForLog(attributeName, context)}.`));
|
|
24433
|
+
}
|
|
24434
|
+
if (this.strictGetAtt) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resourceType}: attributes are not enriched for this resource type, and --strict-getatt rejects the physical ID fallback "${this.maskSecretsForLog(physicalId, context)}" (which may not be the value CloudFormation would return). Drop --strict-getatt to fall back with a warning, avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${this.maskSecretsForLog(attributeName, context)}.`));
|
|
23909
24435
|
this.physicalIdFallbackCount++;
|
|
23910
|
-
this.logger.warn(`Unknown attribute ${attributeName} for resource type ${resourceType}, returning physical ID`);
|
|
24436
|
+
this.logger.warn(`Unknown attribute ${this.maskSecretsForLog(attributeName, context)} for resource type ${resourceType}, returning physical ID`);
|
|
23911
24437
|
return physicalId;
|
|
23912
24438
|
}
|
|
23913
24439
|
/**
|
|
@@ -23920,7 +24446,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23920
24446
|
let values = rawValues;
|
|
23921
24447
|
if (!Array.isArray(values)) values = await this.resolveValue(values, context);
|
|
23922
24448
|
if (!Array.isArray(values)) throw new Error(`Fn::Join's second argument must be a list (an array literal or a list-returning intrinsic such as Fn::Cidr / Fn::GetAZs / Fn::Split / a Ref to a list-typed parameter — any List<...> type or CommaDelimitedList), but resolved to ${typeof values}`);
|
|
23923
|
-
let result = (await
|
|
24449
|
+
let result = (await allSettledKeepingFirstRejection(values.map(async (v) => {
|
|
23924
24450
|
const resolved = await this.resolveValue(v, context);
|
|
23925
24451
|
return String(resolved);
|
|
23926
24452
|
}))).join(delimiter);
|
|
@@ -24108,13 +24634,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24108
24634
|
} catch (getAttError) {
|
|
24109
24635
|
if (getAttError instanceof IntrinsicResolutionRefusalError) throw getAttError;
|
|
24110
24636
|
this.rethrowStructuralSubFailure(varNameStr, getAttError, context);
|
|
24111
|
-
this.logger.warn(this.subPlaceholderWarning(varNameStr, getAttError));
|
|
24637
|
+
this.logger.warn(this.maskSecretsForLog(this.subPlaceholderWarning(varNameStr, getAttError), context));
|
|
24112
24638
|
replacement = match[0];
|
|
24113
24639
|
}
|
|
24114
24640
|
else {
|
|
24115
24641
|
if (refError instanceof IntrinsicResolutionRefusalError) throw refError;
|
|
24116
24642
|
this.rethrowStructuralSubFailure(varNameStr, refError, context);
|
|
24117
|
-
this.logger.warn(this.subPlaceholderWarning(varNameStr, refError));
|
|
24643
|
+
this.logger.warn(this.maskSecretsForLog(this.subPlaceholderWarning(varNameStr, refError), context));
|
|
24118
24644
|
replacement = match[0];
|
|
24119
24645
|
}
|
|
24120
24646
|
}
|
|
@@ -24148,7 +24674,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24148
24674
|
return `{{Fn::Select:${index}:OutOfBounds}}`;
|
|
24149
24675
|
}
|
|
24150
24676
|
const result = resolvedList[index];
|
|
24151
|
-
this.logger.debug(`Resolved Fn::Select: index ${index} -> ${
|
|
24677
|
+
this.logger.debug(`Resolved Fn::Select: index ${index} -> ${JSON.stringify(this.maskValueLeaves(result, context))}`);
|
|
24152
24678
|
return result;
|
|
24153
24679
|
}
|
|
24154
24680
|
/**
|
|
@@ -24292,7 +24818,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24292
24818
|
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Fn::Split: the value to split${sourceClause} must be a string, got ${resolvedValue === null ? "null" : typeof resolvedValue}. Fn::Split accepts only a string; check the value or the intrinsic that produced it.`));
|
|
24293
24819
|
}
|
|
24294
24820
|
const result = resolvedValue.split(delimiter);
|
|
24295
|
-
this.logger.debug(`Resolved Fn::Split: split by "${delimiter}" -> ${
|
|
24821
|
+
this.logger.debug(`Resolved Fn::Split: split by "${delimiter}" -> ${JSON.stringify(this.maskValueLeaves(result, context))}`);
|
|
24296
24822
|
return result;
|
|
24297
24823
|
}
|
|
24298
24824
|
/**
|
|
@@ -24323,7 +24849,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24323
24849
|
const resolved1 = await this.resolveValue(value1, context);
|
|
24324
24850
|
const resolved2 = await this.resolveValue(value2, context);
|
|
24325
24851
|
const result = JSON.stringify(resolved1) === JSON.stringify(resolved2);
|
|
24326
|
-
this.logger.debug(`Resolved Fn::Equals: ${
|
|
24852
|
+
this.logger.debug(`Resolved Fn::Equals: ${JSON.stringify(this.maskValueLeaves(resolved1, context))} === ${JSON.stringify(this.maskValueLeaves(resolved2, context))} -> ${result}`);
|
|
24327
24853
|
return result;
|
|
24328
24854
|
}
|
|
24329
24855
|
/**
|
|
@@ -24471,7 +24997,10 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24471
24997
|
if (context.recordedSecretValues) recordMaskOnlyValuesIn(recovered, context.recordedSecretValues);
|
|
24472
24998
|
return recovered;
|
|
24473
24999
|
}
|
|
24474
|
-
if (context.redactedAttributeReads !== void 0
|
|
25000
|
+
if (context.redactedAttributeReads !== void 0) {
|
|
25001
|
+
const loggedOrigin = this.maskSecretsForLog(origin, context);
|
|
25002
|
+
if (!context.redactedAttributeReads.includes(loggedOrigin)) context.redactedAttributeReads.push(loggedOrigin);
|
|
25003
|
+
}
|
|
24475
25004
|
}
|
|
24476
25005
|
if (!carriesDynamicReference(value)) return value;
|
|
24477
25006
|
const resolver = this.resolverForProducerRegion(producerRegion);
|
|
@@ -24587,12 +25116,12 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24587
25116
|
try {
|
|
24588
25117
|
entry = await context.exportIndex.lookup(exportName);
|
|
24589
25118
|
} catch (err) {
|
|
24590
|
-
this.logger.warn(`Exports index lookup failed for '${loggedExportName}': ${err instanceof Error ? err.message : String(err)}; falling back to state.json scan`);
|
|
25119
|
+
this.logger.warn(`Exports index lookup failed for '${loggedExportName}': ${this.maskSecretsForLog(err instanceof Error ? err.message : String(err), context)}; falling back to state.json scan`);
|
|
24591
25120
|
entry = void 0;
|
|
24592
25121
|
}
|
|
24593
25122
|
if (entry && (!context.stackName || entry.producerStack !== context.stackName)) {
|
|
24594
25123
|
this.recordImport(context, exportName, entry.producerStack, entry.producerRegion);
|
|
24595
|
-
this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from index: ${entry.producerStack} / ${entry.producerRegion}; ${carriesDynamicReference(entry.value) ? "redacted dynamic reference" : "literal value"})`);
|
|
25124
|
+
this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from index: ${this.maskSecretsForLog(entry.producerStack, context)} / ${entry.producerRegion}; ${carriesDynamicReference(entry.value) ? "redacted dynamic reference" : "literal value"})`);
|
|
24596
25125
|
return await this.reresolveCrossStackValue(entry.value, entry.producerRegion, context, `Fn::ImportValue '${exportName}' (producer ${entry.producerStack} / ${entry.producerRegion})`, sourceKey, {
|
|
24597
25126
|
stackName: entry.producerStack,
|
|
24598
25127
|
region: entry.producerRegion,
|
|
@@ -24606,30 +25135,30 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24606
25135
|
for (const ref of allStacks) {
|
|
24607
25136
|
const { stackName: refStack, region: refRegion } = ref;
|
|
24608
25137
|
if (context.stackName && refStack === context.stackName) {
|
|
24609
|
-
this.logger.debug(`Skipping current stack: ${refStack}`);
|
|
25138
|
+
this.logger.debug(`Skipping current stack: ${this.maskSecretsForLog(refStack, context)}`);
|
|
24610
25139
|
continue;
|
|
24611
25140
|
}
|
|
24612
25141
|
try {
|
|
24613
25142
|
const lookupRegion = refRegion ?? this.resolverRegion ?? "";
|
|
24614
25143
|
if (!lookupRegion) {
|
|
24615
|
-
this.logger.debug(`No region available for stack '${refStack}' — skipping (cdkd cannot read state without a region)`);
|
|
25144
|
+
this.logger.debug(`No region available for stack '${this.maskSecretsForLog(refStack, context)}' — skipping (cdkd cannot read state without a region)`);
|
|
24616
25145
|
continue;
|
|
24617
25146
|
}
|
|
24618
25147
|
const stateData = await context.stateBackend.getState(refStack, lookupRegion);
|
|
24619
25148
|
if (!stateData) {
|
|
24620
|
-
this.logger.debug(`No state found for stack: ${refStack} (${lookupRegion})`);
|
|
25149
|
+
this.logger.debug(`No state found for stack: ${this.maskSecretsForLog(refStack, context)} (${lookupRegion})`);
|
|
24621
25150
|
continue;
|
|
24622
25151
|
}
|
|
24623
25152
|
const { state } = stateData;
|
|
24624
25153
|
if (importableOutputKeys(state).includes(exportName)) {
|
|
24625
25154
|
const value = state.outputs[exportName];
|
|
24626
|
-
this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from stack: ${refStack} / ${lookupRegion}; ${carriesDynamicReference(value) ? "redacted dynamic reference" : "literal value"})`);
|
|
25155
|
+
this.logger.info(`Resolved Fn::ImportValue: ${loggedExportName} (from stack: ${this.maskSecretsForLog(refStack, context)} / ${lookupRegion}; ${carriesDynamicReference(value) ? "redacted dynamic reference" : "literal value"})`);
|
|
24627
25156
|
if (context.exportIndex) context.exportIndex.patchEntry(exportName, {
|
|
24628
25157
|
value,
|
|
24629
25158
|
producerStack: refStack,
|
|
24630
25159
|
producerRegion: lookupRegion
|
|
24631
25160
|
}).catch((err) => {
|
|
24632
|
-
this.logger.debug(`Failed to patch exports index for '${exportName}': ${err instanceof Error ? err.message : String(err)}`);
|
|
25161
|
+
this.logger.debug(`Failed to patch exports index for '${this.maskSecretsForLog(exportName, context)}': ${this.maskSecretsForLog(err instanceof Error ? err.message : String(err), context)}`);
|
|
24633
25162
|
});
|
|
24634
25163
|
this.recordImport(context, exportName, refStack, lookupRegion);
|
|
24635
25164
|
found = {
|
|
@@ -24640,7 +25169,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24640
25169
|
break;
|
|
24641
25170
|
}
|
|
24642
25171
|
} catch (error) {
|
|
24643
|
-
this.logger.warn(`Failed to read state for stack ${refStack}: ${error instanceof Error ? error.message : String(error)}`);
|
|
25172
|
+
this.logger.warn(`Failed to read state for stack ${this.maskSecretsForLog(refStack, context)}: ${this.maskSecretsForLog(error instanceof Error ? error.message : String(error), context)}`);
|
|
24644
25173
|
continue;
|
|
24645
25174
|
}
|
|
24646
25175
|
}
|
|
@@ -24656,7 +25185,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24656
25185
|
return cfnExport.value;
|
|
24657
25186
|
}
|
|
24658
25187
|
}
|
|
24659
|
-
throw new Error(`Fn::ImportValue: export '${
|
|
25188
|
+
throw new Error(`Fn::ImportValue: export '${loggedExportName}' not found in any stack. Searched ${allStacks.length} cdkd state record(s)${this.cfnFallback ? " and CloudFormation exports" : ""}. Make sure the exporting stack has been deployed and the Output has an Export.Name property.`);
|
|
24660
25189
|
}
|
|
24661
25190
|
/**
|
|
24662
25191
|
* CloudFormation `ListExports` fallback lookup for `Fn::ImportValue`
|
|
@@ -24687,7 +25216,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24687
25216
|
};
|
|
24688
25217
|
return;
|
|
24689
25218
|
} catch (error) {
|
|
24690
|
-
this.logger.warn(`Fn::ImportValue: CloudFormation ListExports fallback failed for export '${this.maskSecretsForLog(exportName, context)}' (region ${this.resolverRegion}): ${error instanceof Error ? error.message : String(error)}. Grant cloudformation:ListExports to resolve exports from CloudFormation-managed stacks, or pass --no-cfn-fallback to disable the fallback.`);
|
|
25219
|
+
this.logger.warn(`Fn::ImportValue: CloudFormation ListExports fallback failed for export '${this.maskSecretsForLog(exportName, context)}' (region ${this.resolverRegion}): ${this.maskSecretsForLog(error instanceof Error ? error.message : String(error), context)}. Grant cloudformation:ListExports to resolve exports from CloudFormation-managed stacks, or pass --no-cfn-fallback to disable the fallback.`);
|
|
24691
25220
|
return;
|
|
24692
25221
|
}
|
|
24693
25222
|
}
|
|
@@ -24757,7 +25286,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24757
25286
|
return await fetch;
|
|
24758
25287
|
} catch (error) {
|
|
24759
25288
|
const message = error instanceof Error ? error.message : String(error);
|
|
24760
|
-
this.logger.warn(`Fn::GetStackOutput: CloudFormation DescribeStacks fallback failed for stack '${this.maskSecretsForLog(stackName, context)}' (${region}): ${message}. Grant cloudformation:DescribeStacks to resolve outputs from CloudFormation-managed stacks, or pass --no-cfn-fallback to disable the fallback.`);
|
|
25289
|
+
this.logger.warn(`Fn::GetStackOutput: CloudFormation DescribeStacks fallback failed for stack '${this.maskSecretsForLog(stackName, context)}' (${this.maskSecretsForLog(region, context)}): ${this.maskSecretsForLog(message, context)}. Grant cloudformation:DescribeStacks to resolve outputs from CloudFormation-managed stacks, or pass --no-cfn-fallback to disable the fallback.`);
|
|
24761
25290
|
return;
|
|
24762
25291
|
}
|
|
24763
25292
|
}
|
|
@@ -24866,7 +25395,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24866
25395
|
const resolvedRegion = await this.resolveValue(args["Region"], context);
|
|
24867
25396
|
if (typeof resolvedRegion !== "string" || resolvedRegion === "") throw new Error(`Fn::GetStackOutput: Region must resolve to a non-empty string, got ${typeof resolvedRegion}`);
|
|
24868
25397
|
const requestedRegion = canonicalizeRegion(resolvedRegion);
|
|
24869
|
-
if (!isClientSafeRegion(requestedRegion)) throw new Error(`Fn::GetStackOutput: '${
|
|
25398
|
+
if (!isClientSafeRegion(requestedRegion)) throw new Error(`Fn::GetStackOutput: '${this.maskThenStripThenMask(resolvedRegion, context).slice(0, 64)}' is not a valid AWS region name. The region selects both the AWS endpoint and the state-file key, so cdkd will not use it.`);
|
|
24870
25399
|
region = requestedRegion;
|
|
24871
25400
|
}
|
|
24872
25401
|
let roleArn;
|
|
@@ -24875,10 +25404,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24875
25404
|
if (typeof raw !== "string" || raw === "") throw new Error(`Fn::GetStackOutput: RoleArn must be a literal string in the template (no Ref / Fn::GetAtt / Fn::Sub allowed for cross-account references). Got ${raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw}${typeof raw === "object" ? ` (intrinsic shape: ${JSON.stringify(raw).slice(0, 80)})` : ""}.`);
|
|
24876
25405
|
roleArn = raw;
|
|
24877
25406
|
}
|
|
24878
|
-
if (!roleArn && context.stackName && context.stackName === stackName && canonicalizeRegion(region) === canonicalizeRegion(this.resolverRegion)) throw new Error(`Fn::GetStackOutput: cannot reference own stack '${stackName}' in the same region '${region}'`);
|
|
25407
|
+
if (!roleArn && context.stackName && context.stackName === stackName && canonicalizeRegion(region) === canonicalizeRegion(this.resolverRegion)) throw new Error(`Fn::GetStackOutput: cannot reference own stack '${this.maskSecretsForLog(stackName, context)}' in the same region '${this.maskSecretsForLog(region, context)}'`);
|
|
24879
25408
|
const loggedStackName = this.maskSecretsForLog(stackName, context);
|
|
24880
25409
|
const loggedOutputName = this.maskSecretsForLog(outputName, context);
|
|
24881
|
-
this.
|
|
25410
|
+
const loggedRegion = this.maskSecretsForLog(region, context);
|
|
25411
|
+
this.logger.debug(`Resolving Fn::GetStackOutput: StackName=${loggedStackName}, Region=${loggedRegion}, OutputName=${loggedOutputName}${roleArn ? `, RoleArn=${roleArn}` : ""}`);
|
|
24882
25412
|
const stateData = roleArn ? await this.getCrossAccountStackState(roleArn, stackName, region, context) : await this.getSameAccountStackState(stackName, region, context);
|
|
24883
25413
|
if (!stateData) {
|
|
24884
25414
|
if (!roleArn && this.cfnFallback) {
|
|
@@ -24886,24 +25416,24 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24886
25416
|
if (cfnOutputs) {
|
|
24887
25417
|
if (!Object.hasOwn(cfnOutputs, outputName)) {
|
|
24888
25418
|
const available = this.describeAvailableOutputs(Object.keys(cfnOutputs), context);
|
|
24889
|
-
throw new Error(`Fn::GetStackOutput: output '${
|
|
25419
|
+
throw new Error(`Fn::GetStackOutput: output '${loggedOutputName}' not found in CloudFormation stack '${loggedStackName}' (${loggedRegion}). Available outputs: ${available}`);
|
|
24890
25420
|
}
|
|
24891
25421
|
const value = cfnOutputs[outputName];
|
|
24892
|
-
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${
|
|
25422
|
+
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${loggedRegion}, OutputName=${loggedOutputName} (from CloudFormation stack outputs; weak reference — producer is not cdkd-managed)`);
|
|
24893
25423
|
return value;
|
|
24894
25424
|
}
|
|
24895
25425
|
}
|
|
24896
|
-
throw new Error(`Fn::GetStackOutput: stack '${
|
|
25426
|
+
throw new Error(`Fn::GetStackOutput: stack '${loggedStackName}' not found in region '${loggedRegion}'${roleArn ? ` (cross-account via ${roleArn})` : ""}. ${!roleArn && this.cfnFallback ? "Searched cdkd state and CloudFormation stacks. Make sure the producer stack has been deployed (via cdkd or CloudFormation)." : `Make sure the producer stack has been deployed via cdkd.`}`);
|
|
24897
25427
|
}
|
|
24898
25428
|
const outputs = stateData.state.outputs ?? {};
|
|
24899
25429
|
if (!Object.hasOwn(outputs, outputName)) {
|
|
24900
25430
|
const available = this.describeAvailableOutputs(Object.keys(outputs), context);
|
|
24901
|
-
throw new Error(`Fn::GetStackOutput: output '${
|
|
25431
|
+
throw new Error(`Fn::GetStackOutput: output '${loggedOutputName}' not found in stack '${loggedStackName}' (${loggedRegion}). Available outputs: ${available}`);
|
|
24902
25432
|
}
|
|
24903
25433
|
const value = outputs[outputName];
|
|
24904
|
-
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${
|
|
25434
|
+
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${loggedStackName}, Region=${loggedRegion}, OutputName=${loggedOutputName}${roleArn ? `, RoleArn=${roleArn}` : ""} (${carriesDynamicReference(value) ? "redacted dynamic reference" : "literal value"})`);
|
|
24905
25435
|
if (!roleArn) this.recordOutputRead(context, stackName, region, outputName);
|
|
24906
|
-
if (roleArn && !context.skipDynamicReferences && carriesDynamicReference(value)) throw markNonRetryable(new CrossAccountSecretRefusalError(`Fn::GetStackOutput: output '${
|
|
25436
|
+
if (roleArn && !context.skipDynamicReferences && carriesDynamicReference(value)) throw markNonRetryable(new CrossAccountSecretRefusalError(`Fn::GetStackOutput: output '${loggedOutputName}' of stack '${loggedStackName}' (${loggedRegion}) is a redacted dynamic reference, and this is a CROSS-ACCOUNT reference (RoleArn ${roleArn}). cdkd will not resolve a producer account's secret with the consumer's credentials — a same-named secret in the consumer account would answer instead. Export a non-secret value (e.g. the secret's ARN) and resolve it in the consumer stack, or reference the producer stack from within its own account.`));
|
|
24907
25437
|
return await this.reresolveCrossStackValue(value, region, context, `Fn::GetStackOutput '${outputName}' (producer ${stackName} / ${region})`, sourceKey, {
|
|
24908
25438
|
stackName,
|
|
24909
25439
|
region,
|
|
@@ -25021,19 +25551,19 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25021
25551
|
}
|
|
25022
25552
|
if (!map) {
|
|
25023
25553
|
if (hasDefaultValue) return await resolveDefault();
|
|
25024
|
-
throw new Error(`Fn::FindInMap: mapping '${mapName}' not found in Mappings section`);
|
|
25554
|
+
throw new Error(`Fn::FindInMap: mapping '${this.maskSecretsForLog(mapName, context)}' not found in Mappings section`);
|
|
25025
25555
|
}
|
|
25026
25556
|
const topLevel = Object.hasOwn(map, topLevelKey) ? map[topLevelKey] : void 0;
|
|
25027
25557
|
if (!topLevel || typeof topLevel !== "object") {
|
|
25028
25558
|
if (hasDefaultValue) return await resolveDefault();
|
|
25029
|
-
throw new Error(`Fn::FindInMap: top-level key '${topLevelKey}' not found in mapping '${mapName}'`);
|
|
25559
|
+
throw new Error(`Fn::FindInMap: top-level key '${this.maskSecretsForLog(topLevelKey, context)}' not found in mapping '${this.maskSecretsForLog(mapName, context)}'`);
|
|
25030
25560
|
}
|
|
25031
25561
|
if (!Object.hasOwn(topLevel, secondLevelKey)) {
|
|
25032
25562
|
if (hasDefaultValue) return await resolveDefault();
|
|
25033
|
-
throw new Error(`Fn::FindInMap: second-level key '${secondLevelKey}' not found in mapping '${mapName}' -> '${topLevelKey}'`);
|
|
25563
|
+
throw new Error(`Fn::FindInMap: second-level key '${this.maskSecretsForLog(secondLevelKey, context)}' not found in mapping '${this.maskSecretsForLog(mapName, context)}' -> '${this.maskSecretsForLog(topLevelKey, context)}'`);
|
|
25034
25564
|
}
|
|
25035
25565
|
const result = topLevel[secondLevelKey];
|
|
25036
|
-
this.logger.debug(`Resolved Fn::FindInMap: ${mapName}.${topLevelKey}.${secondLevelKey} -> ${JSON.stringify(result)}`);
|
|
25566
|
+
this.logger.debug(`Resolved Fn::FindInMap: ${this.maskSecretsForLog(mapName, context)}.${this.maskSecretsForLog(topLevelKey, context)}.${this.maskSecretsForLog(secondLevelKey, context)} -> ${this.maskSecretsForLog(JSON.stringify(this.maskValueLeaves(result, context)), context)}`);
|
|
25037
25567
|
return result;
|
|
25038
25568
|
}
|
|
25039
25569
|
/**
|
|
@@ -25046,6 +25576,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25046
25576
|
const resolvedValue = await this.resolveValue(value, context);
|
|
25047
25577
|
if (typeof resolvedValue !== "string") throw new Error(`Fn::Base64: value must resolve to a string, got ${typeof resolvedValue}`);
|
|
25048
25578
|
const result = Buffer.from(resolvedValue).toString("base64");
|
|
25579
|
+
if (context.recordedSecretValues && this.maskSecretsForLog(resolvedValue, context) !== resolvedValue) recordMaskOnlyValue(context.recordedSecretValues, result);
|
|
25049
25580
|
this.logger.debug(`Resolved Fn::Base64: ${this.maskSecretsForLog(resolvedValue, context)} -> ${this.maskSecretsForLog(result, context)}`);
|
|
25050
25581
|
return result;
|
|
25051
25582
|
}
|
|
@@ -25077,7 +25608,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25077
25608
|
let clientRegion;
|
|
25078
25609
|
if (typeof resolvedValue === "string" && resolvedValue !== "") {
|
|
25079
25610
|
const requested = canonicalizeRegion(resolvedValue);
|
|
25080
|
-
if (!isClientSafeRegion(requested)) throw new Error(`Fn::GetAZs: '${
|
|
25611
|
+
if (!isClientSafeRegion(requested)) throw new Error(`Fn::GetAZs: '${this.maskThenStripThenMask(resolvedValue, context).slice(0, 64)}' is not a valid AWS region name. A region is substituted into the AWS service hostname, so cdkd will not build a client from it.`);
|
|
25081
25612
|
region = requested;
|
|
25082
25613
|
clientRegion = requested;
|
|
25083
25614
|
} else {
|
|
@@ -25086,7 +25617,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25086
25617
|
}
|
|
25087
25618
|
const cached = cachedAvailabilityZones[region];
|
|
25088
25619
|
if (cached) {
|
|
25089
|
-
this.logger.debug(`Resolved Fn::GetAZs from cache: ${region} -> ${JSON.stringify(cached)}`);
|
|
25620
|
+
this.logger.debug(`Resolved Fn::GetAZs from cache: ${this.maskSecretsForLog(region, context)} -> ${JSON.stringify(this.maskValueLeaves(cached, context))}`);
|
|
25090
25621
|
return cached;
|
|
25091
25622
|
}
|
|
25092
25623
|
const ec2Client = this.clientsForRegion(clientRegion).ec2;
|
|
@@ -25100,11 +25631,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25100
25631
|
Values: ["available"]
|
|
25101
25632
|
}] }))).AvailabilityZones || []).map((az) => az.ZoneName).filter((name) => name !== void 0).sort();
|
|
25102
25633
|
} catch (error) {
|
|
25103
|
-
throw new Error(`Fn::GetAZs: failed to describe availability zones for region '${region}': ${error instanceof Error ? error.message : String(error)}`);
|
|
25634
|
+
throw new Error(`Fn::GetAZs: failed to describe availability zones for region '${this.maskSecretsForLog(region, context)}': ${this.maskSecretsForLog(error instanceof Error ? error.message : String(error), context)}`);
|
|
25104
25635
|
}
|
|
25105
|
-
if (azNames.length === 0) throw new Error(`Fn::GetAZs: no availability zones returned for region '${region}'. Either the region is not enabled on this account (opt-in regions must be enabled before use), or the request was answered by a different region's endpoint.`);
|
|
25636
|
+
if (azNames.length === 0) throw new Error(`Fn::GetAZs: no availability zones returned for region '${this.maskSecretsForLog(region, context)}'. Either the region is not enabled on this account (opt-in regions must be enabled before use), or the request was answered by a different region's endpoint.`);
|
|
25106
25637
|
cachedAvailabilityZones[region] = azNames;
|
|
25107
|
-
this.logger.debug(`Resolved Fn::GetAZs: ${region} -> ${JSON.stringify(azNames)}`);
|
|
25638
|
+
this.logger.debug(`Resolved Fn::GetAZs: ${this.maskSecretsForLog(region, context)} -> ${JSON.stringify(this.maskValueLeaves(azNames, context))}`);
|
|
25108
25639
|
return azNames;
|
|
25109
25640
|
}
|
|
25110
25641
|
/**
|
|
@@ -25150,6 +25681,95 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25150
25681
|
if (secrets && secrets.size > 0) masked = maskSecretsInText(masked, secrets);
|
|
25151
25682
|
return masked;
|
|
25152
25683
|
}
|
|
25684
|
+
/**
|
|
25685
|
+
* A copy of `value` with every string LEAF (and every object KEY) masked, for
|
|
25686
|
+
* a caller about to ENCODE it into a message (issue
|
|
25687
|
+
* [#2759](https://github.com/go-to-k/cdkd/issues/2759)).
|
|
25688
|
+
*
|
|
25689
|
+
* `stringifyValue` / `JSON.stringify` ESCAPE a leaf containing `"`, `\` or a
|
|
25690
|
+
* control character, and {@link maskSecretsInText} matches a needle
|
|
25691
|
+
* LITERALLY — so masking the ENCODED text misses exactly the plaintexts the
|
|
25692
|
+
* encoder rewrote (`pa"ss\word12` encodes to `["pa\"ss\\word12"]`, which no
|
|
25693
|
+
* needle matches). Masking each leaf first also buys the WHOLE-VALUE arm,
|
|
25694
|
+
* which has no {@link MIN_NEEDLE_LENGTH} floor, for a leaf that IS the
|
|
25695
|
+
* plaintext.
|
|
25696
|
+
*
|
|
25697
|
+
* Returns the STRUCTURE rather than a rendered string, deliberately: each
|
|
25698
|
+
* call site keeps its own encoder, so this changes which characters are
|
|
25699
|
+
* masked and nothing about how a value RENDERS. Encoding here instead
|
|
25700
|
+
* dropped `JSON.stringify`'s quotes around a bare string and made a `cdkd
|
|
25701
|
+
* scrub` log line unrecognisable to its own test.
|
|
25702
|
+
*
|
|
25703
|
+
* Object KEYS are masked too: a `Fn::Split` / `Fn::GetAtt` chain can put a
|
|
25704
|
+
* resolved value in key position, and an unmasked key discloses exactly as
|
|
25705
|
+
* much as an unmasked value.
|
|
25706
|
+
*
|
|
25707
|
+
* Cycle-safe by MEMOIZATION rather than a depth cap: a self-referential
|
|
25708
|
+
* structure terminates (the replacement is registered before its children are
|
|
25709
|
+
* walked, so the cycle closes on it) and a legal deep one is still walked to
|
|
25710
|
+
* the bottom. A repeated but NON-cyclic sub-object gets its real rendering
|
|
25711
|
+
* rather than a placeholder — see the note at the `Map`.
|
|
25712
|
+
*/
|
|
25713
|
+
maskValueLeaves(value, context) {
|
|
25714
|
+
const done = /* @__PURE__ */ new Map();
|
|
25715
|
+
const walk = (node) => {
|
|
25716
|
+
if (typeof node === "string") return this.maskSecretsForLog(node, context);
|
|
25717
|
+
if (node === null || typeof node !== "object") return node;
|
|
25718
|
+
const memo = done.get(node);
|
|
25719
|
+
if (memo !== void 0) return memo;
|
|
25720
|
+
if (Array.isArray(node)) {
|
|
25721
|
+
const out = [];
|
|
25722
|
+
done.set(node, out);
|
|
25723
|
+
for (const item of node) out.push(walk(item));
|
|
25724
|
+
return out;
|
|
25725
|
+
}
|
|
25726
|
+
const out = Object.create(null);
|
|
25727
|
+
done.set(node, out);
|
|
25728
|
+
for (const [key, child] of Object.entries(node)) out[this.maskSecretsForLog(key, context)] = walk(child);
|
|
25729
|
+
return out;
|
|
25730
|
+
};
|
|
25731
|
+
return walk(value);
|
|
25732
|
+
}
|
|
25733
|
+
/**
|
|
25734
|
+
* Mask `value`, STRIP its control characters, then mask again — the shape a
|
|
25735
|
+
* message that truncates its input needs (issue
|
|
25736
|
+
* [#2827](https://github.com/go-to-k/cdkd/issues/2827) review round 1).
|
|
25737
|
+
*
|
|
25738
|
+
* NEITHER SINGLE ORDER IS CORRECT, and both were measured. Masking AFTER
|
|
25739
|
+
* `stripControlChars` is what this fix was written to avoid: the strip
|
|
25740
|
+
* rewrites the text a literal needle has to match. But masking BEFORE it is
|
|
25741
|
+
* not safe either, because `stripControlChars` DELETES rather than replaces —
|
|
25742
|
+
* so a plaintext SPLIT by an invisible (`S3cret\u200ePassw0rd`) is missed by
|
|
25743
|
+
* the first mask and then RECONSTITUTED contiguous by the strip. That is the
|
|
25744
|
+
* go-to-k/cdkd#2874 class arriving through a different door.
|
|
25745
|
+
*
|
|
25746
|
+
* Masking in BOTH string spaces closes both: the first pass catches a needle
|
|
25747
|
+
* that occurs literally, the second catches one that only becomes contiguous
|
|
25748
|
+
* after stripping. `maskSecretsInText` is idempotent, so the overlap costs
|
|
25749
|
+
* nothing, and the caller truncates AFTERWARDS — never between the two.
|
|
25750
|
+
*
|
|
25751
|
+
* WHICH HALF IS FENCED, stated because a mutation probe made the difference
|
|
25752
|
+
* visible. The FIRST mask is demonstrated by a test: deleting it (masking
|
|
25753
|
+
* only after the strip) reds the split-needle case, because the recorded
|
|
25754
|
+
* needle is then the split form and the strip has destroyed it. The SECOND
|
|
25755
|
+
* mask is NOT reached by any test here and is defence in depth: it earns its
|
|
25756
|
+
* place only when the bag holds a needle that is the STRIPPED form of the
|
|
25757
|
+
* value in hand, and every route through THIS resolver records the value it
|
|
25758
|
+
* actually resolved — so the split copy is itself a needle and the first
|
|
25759
|
+
* mask already catches it. The shape was measured against a hand-built bag
|
|
25760
|
+
* during review, not produced by the resolver. Kept anyway: it is one
|
|
25761
|
+
* idempotent call, and the alternative is re-deciding per future caller
|
|
25762
|
+
* whether the bag and the value can disagree.
|
|
25763
|
+
*
|
|
25764
|
+
* THE BOUND, since this file's job is to state them: this covers a needle
|
|
25765
|
+
* split by a character `stripControlChars` removes. A needle split by
|
|
25766
|
+
* anything else, or one whose canonical form differs for another reason, is
|
|
25767
|
+
* `outputs-export-alias.ts`'s `canonicalForSecretScan` problem and is not
|
|
25768
|
+
* solved here.
|
|
25769
|
+
*/
|
|
25770
|
+
maskThenStripThenMask(value, context) {
|
|
25771
|
+
return this.maskSecretsForLog(stripControlChars(this.maskSecretsForLog(value, context)), context);
|
|
25772
|
+
}
|
|
25153
25773
|
async resolveDynamicReferences(value, context) {
|
|
25154
25774
|
const pattern = /\{\{resolve:([^}]+)\}\}/g;
|
|
25155
25775
|
let result = value;
|
|
@@ -25164,7 +25784,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25164
25784
|
const isKnownSecret = service === "secretsmanager" || service === "ssm-secure" || recordedSecretExpressions.has(fullMatch);
|
|
25165
25785
|
if (isKnownSecret && context?.skipDynamicReferences) continue;
|
|
25166
25786
|
const regionVerdict = classifyReplaySecretRegion(fullMatch, this.explicitRegion ?? this.resolverRegion, context?.producerRegions);
|
|
25167
|
-
if (regionVerdict.kind === "ambiguous") throw markNonRetryable(new DynamicReferenceRegionAmbiguousError(`Refusing to resolve the secret reference ${fullMatch}: it names '${regionVerdict.secretName}' without a region, and this stack reads from ${regionVerdict.foreignProducerRegions.join(", ")} as well as its own region. cdkd cannot tell which one must answer, and resolving against the wrong one yields a different secret. Spell the reference as a full ARN to say which region owns it.`));
|
|
25787
|
+
if (regionVerdict.kind === "ambiguous") throw markNonRetryable(new DynamicReferenceRegionAmbiguousError(`Refusing to resolve the secret reference ${this.maskSecretsForLog(fullMatch, context)}: it names '${this.maskSecretsForLog(regionVerdict.secretName, context)}' without a region, and this stack reads from ${regionVerdict.foreignProducerRegions.join(", ")} as well as its own region. cdkd cannot tell which one must answer, and resolving against the wrong one yields a different secret. Spell the reference as a full ARN to say which region owns it.`));
|
|
25168
25788
|
if (regionVerdict.kind === "named-region") {
|
|
25169
25789
|
const foreign = await this.resolverForProducerRegion(regionVerdict.region).resolveDynamicReferences(fullMatch, withoutProducerRegions(context));
|
|
25170
25790
|
result = result.replace(fullMatch, () => foreign);
|
|
@@ -25209,7 +25829,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25209
25829
|
resolved = param.value;
|
|
25210
25830
|
} else if (service === "ssm-secure") {
|
|
25211
25831
|
const param = await this.resolveSSMReference(parts, true, "ssm-secure", context);
|
|
25212
|
-
if (!param.secure) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Refusing to resolve ${fullMatch}: the parameter is a ${param.type} parameter, and the ssm-secure spelling is defined for SecureString parameters only. Reference it as {{resolve:ssm:...}} if it is public configuration.`));
|
|
25832
|
+
if (!param.secure) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Refusing to resolve ${this.maskSecretsForLog(fullMatch, context)}: the parameter is a ${param.type} parameter, and the ssm-secure spelling is defined for SecureString parameters only. Reference it as {{resolve:ssm:...}} if it is public configuration.`));
|
|
25213
25833
|
isSecret = true;
|
|
25214
25834
|
resolved = param.value;
|
|
25215
25835
|
} else {
|
|
@@ -25273,22 +25893,24 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25273
25893
|
} else secretId = afterService;
|
|
25274
25894
|
if (!versionStage) versionStage = "AWSCURRENT";
|
|
25275
25895
|
if (!secretId) throw new Error("Dynamic reference: secretsmanager SECRET_ID is required");
|
|
25276
|
-
this.
|
|
25896
|
+
const loggedSecretId = this.maskSecretsForLog(secretId, context);
|
|
25897
|
+
const loggedJsonKey = this.maskSecretsForLog(jsonKey, context);
|
|
25898
|
+
this.logger.debug(`Resolving dynamic reference: secretsmanager:${loggedSecretId}:SecretString:${loggedJsonKey}:${this.maskSecretsForLog(versionStage, context)}:${this.maskSecretsForLog(versionId, context)}`);
|
|
25277
25899
|
const client = this.clientsForRegion(this.explicitRegion).secretsManager;
|
|
25278
25900
|
const command = new GetSecretValueCommand({
|
|
25279
25901
|
SecretId: secretId,
|
|
25280
25902
|
...versionStage && versionStage !== "" && { VersionStage: versionStage },
|
|
25281
25903
|
...versionId && versionId !== "" && { VersionId: versionId }
|
|
25282
25904
|
});
|
|
25283
|
-
const secretString = (await this.sendWithThrottleRetry(() => client.send(command),
|
|
25284
|
-
if (!secretString) throw new Error(`Dynamic reference: secret '${
|
|
25905
|
+
const secretString = (await this.sendWithThrottleRetry(() => client.send(command), `secretsmanager:${loggedSecretId}`)).SecretString;
|
|
25906
|
+
if (!secretString) throw new Error(`Dynamic reference: secret '${loggedSecretId}' does not contain a SecretString value`);
|
|
25285
25907
|
if (jsonKey) try {
|
|
25286
25908
|
const parsed = JSON.parse(secretString);
|
|
25287
25909
|
const keyValue = Object.hasOwn(parsed, jsonKey) ? parsed[jsonKey] : void 0;
|
|
25288
|
-
if (keyValue === void 0) throw new Error(`Dynamic reference: key '${
|
|
25910
|
+
if (keyValue === void 0) throw new Error(`Dynamic reference: key '${loggedJsonKey}' not found in secret '${loggedSecretId}'`);
|
|
25289
25911
|
return stringifyValue(keyValue);
|
|
25290
25912
|
} catch (error) {
|
|
25291
|
-
if (error instanceof SyntaxError) throw new Error(`Dynamic reference: secret '${
|
|
25913
|
+
if (error instanceof SyntaxError) throw new Error(`Dynamic reference: secret '${loggedSecretId}' is not valid JSON but JSON_KEY '${loggedJsonKey}' was specified`);
|
|
25292
25914
|
throw error;
|
|
25293
25915
|
}
|
|
25294
25916
|
return secretString;
|
|
@@ -25314,8 +25936,8 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25314
25936
|
const ipBlock = await this.resolveValue(rawIpBlock, context);
|
|
25315
25937
|
const count = Number(await this.resolveValue(rawCount, context));
|
|
25316
25938
|
const cidrBits = Number(await this.resolveValue(rawCidrBits, context));
|
|
25317
|
-
if (!ipBlock || typeof ipBlock !== "string") throw new Error(`Fn::Cidr: ipBlock must be a string, got ${typeof ipBlock}: ${JSON.stringify(ipBlock)}`);
|
|
25318
|
-
this.logger.debug(`Resolving Fn::Cidr: ipBlock=${ipBlock}, count=${count}, cidrBits=${cidrBits}`);
|
|
25939
|
+
if (!ipBlock || typeof ipBlock !== "string") throw new Error(`Fn::Cidr: ipBlock must be a string, got ${typeof ipBlock}: ${JSON.stringify(this.maskValueLeaves(ipBlock, context))}`);
|
|
25940
|
+
this.logger.debug(`Resolving Fn::Cidr: ipBlock=${this.maskSecretsForLog(JSON.stringify(this.maskValueLeaves(ipBlock, context)), context)}, count=${count}, cidrBits=${cidrBits}`);
|
|
25319
25941
|
const isIpv6 = ipBlock.includes(":");
|
|
25320
25942
|
const results = [];
|
|
25321
25943
|
if (isIpv6) {
|
|
@@ -25347,7 +25969,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25347
25969
|
results.push(`${a}.${b}.${c}.${d}/${subnetPrefix}`);
|
|
25348
25970
|
}
|
|
25349
25971
|
}
|
|
25350
|
-
this.logger.debug(`Fn::Cidr result: ${JSON.stringify(results)}`);
|
|
25972
|
+
this.logger.debug(`Fn::Cidr result: ${this.maskSecretsForLog(JSON.stringify(this.maskValueLeaves(results, context)), context)}`);
|
|
25351
25973
|
return results;
|
|
25352
25974
|
}
|
|
25353
25975
|
/** Expand IPv6 address to full 8-group form */
|
|
@@ -25437,21 +26059,22 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
25437
26059
|
async resolveSSMReference(parts, decrypt = true, service = "ssm", context) {
|
|
25438
26060
|
const parameterName = parts.slice(1).join(":");
|
|
25439
26061
|
if (!parameterName) throw new Error(`Dynamic reference: ${service} PARAMETER_NAME is required`);
|
|
25440
|
-
this.
|
|
26062
|
+
const loggedParameterName = this.maskSecretsForLog(parameterName, context);
|
|
26063
|
+
this.logger.debug(`Resolving dynamic reference: ${service}:${loggedParameterName}`);
|
|
25441
26064
|
const client = this.clientsForRegion(this.explicitRegion).ssm;
|
|
25442
26065
|
const command = new GetParameterCommand({
|
|
25443
26066
|
Name: parameterName,
|
|
25444
26067
|
WithDecryption: decrypt
|
|
25445
26068
|
});
|
|
25446
|
-
const response = await this.sendWithThrottleRetry(() => client.send(command),
|
|
26069
|
+
const response = await this.sendWithThrottleRetry(() => client.send(command), `${service}:${loggedParameterName}`);
|
|
25447
26070
|
const paramValue = response.Parameter?.Value;
|
|
25448
|
-
if (paramValue === void 0 || paramValue === null) throw new Error(`Dynamic reference: SSM parameter '${
|
|
26071
|
+
if (paramValue === void 0 || paramValue === null) throw new Error(`Dynamic reference: SSM parameter '${loggedParameterName}' not found or has no value`);
|
|
25449
26072
|
const paramType = response.Parameter?.Type;
|
|
25450
26073
|
const secure = paramType !== "String" && paramType !== "StringList";
|
|
25451
26074
|
if (secure && paramType !== "SecureString" && !this.warnedUnrecognizedSsmTypes.has(`${parameterName}\u0000${String(paramType)}`)) {
|
|
25452
26075
|
this.warnedUnrecognizedSsmTypes.add(`${parameterName}\u0000${String(paramType)}`);
|
|
25453
26076
|
const reported = paramType === void 0 ? "(absent)" : `'${String(paramType)}'`;
|
|
25454
|
-
this.logger.warn(
|
|
26077
|
+
this.logger.warn(`SSM parameter '${loggedParameterName}' reported an unrecognized Type ${reported} — treating its value as a secret, so cdkd will persist the {{resolve:${service}:...}} expression rather than the resolved value. Declare the parameter as String / StringList if it is public config.`);
|
|
25455
26078
|
}
|
|
25456
26079
|
return {
|
|
25457
26080
|
value: paramValue,
|
|
@@ -26660,7 +27283,7 @@ var CloudControlProvider = class {
|
|
|
26660
27283
|
const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
|
|
26661
27284
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
26662
27285
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
26663
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
27286
|
+
const { ASGProvider } = await import("./asg-provider-C_AHAmgD.js").then((n) => n.n);
|
|
26664
27287
|
const asgProvider = new ASGProvider();
|
|
26665
27288
|
return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
|
|
26666
27289
|
}
|
|
@@ -31137,6 +31760,134 @@ function isExportAliasCollision(exportName, outputKey, ownedOutputNames) {
|
|
|
31137
31760
|
*/
|
|
31138
31761
|
const MIN_SECRET_NEEDLE = 4;
|
|
31139
31762
|
/**
|
|
31763
|
+
* Characters deleted before any secret containment test on this path, and
|
|
31764
|
+
* deleted from the text that gets PRINTED — ONE class, because the verdict and
|
|
31765
|
+
* the printed text have to live in the same string space (issue
|
|
31766
|
+
* [#2874](https://github.com/go-to-k/cdkd/issues/2874)).
|
|
31767
|
+
*
|
|
31768
|
+
* WHY A THIRD CLASS RATHER THAN EITHER SANITISER'S. `stripControlChars`
|
|
31769
|
+
* DELETES its class; `displaySafe` REPLACES its own with a space. Composing
|
|
31770
|
+
* them — which is what every site here used to do — leaves three different
|
|
31771
|
+
* strings in play: the raw key the verdict was taken from, the sanitised key
|
|
31772
|
+
* that was printed, and the masked one in between. A recorded secret split by
|
|
31773
|
+
* a DELETED character is absent from the raw key and contiguous in the printed
|
|
31774
|
+
* one, so the verdict said `safe` over text that held the plaintext; a secret
|
|
31775
|
+
* split by a REPLACED one is absent from both and prints one character short
|
|
31776
|
+
* of the plaintext, which a `not.toContain(SECRET)` assertion cannot see.
|
|
31777
|
+
* Measured across both classes: eight of ten characters reconstituted the
|
|
31778
|
+
* secret verbatim, and the remaining two printed it minus one character.
|
|
31779
|
+
*
|
|
31780
|
+
* So the fix is not another arm on the check — it is removing the second and
|
|
31781
|
+
* third string. Everything below happens in `canonicalForSecretScan` space.
|
|
31782
|
+
*
|
|
31783
|
+
* THE CLASS IS DERIVED FROM UNICODE CATEGORIES, NOT ENUMERATED. A hand list
|
|
31784
|
+
* was written first -- both sanitisers' classes plus the four residuals
|
|
31785
|
+
* `display-safe.ts` names -- and review measured it arbitrary: `U+2060`
|
|
31786
|
+
* (WORD JOINER) verdicted `safe` and printed visibly-contiguous plaintext
|
|
31787
|
+
* while `U+FEFF` was caught, and `U+2060` is the character Unicode itself
|
|
31788
|
+
* designates as the replacement for `U+FEFF` in that role. Eight more did the
|
|
31789
|
+
* same (`U+00AD`, `U+180E`, `U+FE0F`, `U+3164`, `U+2061`, `U+FFFB`, `U+115F`,
|
|
31790
|
+
* `U+17B4`). A list of the invisibles somebody happened to think of is not a
|
|
31791
|
+
* boundary; the general categories are.
|
|
31792
|
+
*
|
|
31793
|
+
* - `\p{Cc}` control and `\p{Cf}` format: the C0 / C1 and DEL ranges, the bidi
|
|
31794
|
+
* marks / embeddings / overrides / isolates, the zero-width set, `U+FEFF`,
|
|
31795
|
+
* `U+00AD`, `U+061C`, and the invisible-operator block.
|
|
31796
|
+
* - `\p{Zl}` / `\p{Zp}`: `U+2028` / `U+2029`, the two `displaySafe` REPLACES.
|
|
31797
|
+
* - `\p{Default_Ignorable_Code_Point}`: Unicode's own INTENT-TO-BE-IGNORED
|
|
31798
|
+
* property, which carries the variation selectors, the Mongolian free
|
|
31799
|
+
* variation selectors, the Hangul fillers and `U+034F` COMBINING GRAPHEME
|
|
31800
|
+
* JOINER.
|
|
31801
|
+
*
|
|
31802
|
+
* - `\p{Me}` ENCLOSING marks. The same zero-advance-width shape as `\p{Mn}`
|
|
31803
|
+
* below, and INCLUDED rather than deferred because the cost argument that
|
|
31804
|
+
* defers `\p{Mn}` does not transfer: `\p{Me}` is about a dozen code points
|
|
31805
|
+
* with no legitimate use in a resource name.
|
|
31806
|
+
*
|
|
31807
|
+
* NAMED RESIDUAL, because `\p{Default_Ignorable_Code_Point}` is Unicode's
|
|
31808
|
+
* INTENT-TO-BE-IGNORED property and NOT "everything that renders as nothing"
|
|
31809
|
+
* -- an earlier revision of this comment claimed the latter and review refuted
|
|
31810
|
+
* it. NONSPACING marks (`\p{Mn}`) carry zero advance width, so a secret split
|
|
31811
|
+
* by one renders contiguous while this class keeps it: measured with `U+09BC`,
|
|
31812
|
+
* which verdicts `safe` AND publishes the alias. Not widened here, because
|
|
31813
|
+
* `\p{Mn}` is the diacritics of Devanagari, Arabic, Hebrew and Vietnamese and
|
|
31814
|
+
* deleting it would mangle legitimate names for every user. Tracked as issue
|
|
31815
|
+
* [#2889](https://github.com/go-to-k/cdkd/issues/2889), which also carries the
|
|
31816
|
+
* homoglyph question. `origin/main` behaves identically, so this is a recorded
|
|
31817
|
+
* residual rather than something this change introduced.
|
|
31818
|
+
*
|
|
31819
|
+
* THE THIRD BULLET REPLACED A HAND TAIL, and the hand tail was measured
|
|
31820
|
+
* leaking. A first cut listed the ranges by hand -- `FE00-FE0F`,
|
|
31821
|
+
* `E0100-E01EF`, `180B-180D`, `115F`, `1160`, `3164`, `FFA0`, `17B4`, `17B5`
|
|
31822
|
+
* -- and review found `U+034F` outside it, printing `hunter2hunter2` in a
|
|
31823
|
+
* `warn` line under a `safe` verdict. It also spelled `180B-180D` while
|
|
31824
|
+
* calling itself "the Mongolian FVS", missing `U+180F`, which Unicode 14
|
|
31825
|
+
* added. Naming the PROPERTY is what makes the next such addition arrive
|
|
31826
|
+
* covered instead of arriving as another finding.
|
|
31827
|
+
*
|
|
31828
|
+
* WIDENED HERE AND NOT IN `display-safe.ts` because the two files answer
|
|
31829
|
+
* different questions. `displaySafe` is about a terminal rendering a value,
|
|
31830
|
+
* and its recorded reason for keeping these is command forgery -- where a
|
|
31831
|
+
* character that renders as nothing changes nothing. That reason does not
|
|
31832
|
+
* transfer to a secret: a plaintext split by an invisible character is READ by
|
|
31833
|
+
* a human exactly as if it were contiguous, so disclosure needs no paste.
|
|
31834
|
+
*
|
|
31835
|
+
* `tests/unit/deployment/secret-scan-class-superset.test.ts` fences this as a
|
|
31836
|
+
* SUPERSET of both sanitisers' classes by SCANNING CODE POINTS rather than by
|
|
31837
|
+
* comparing source text, so a future edit to either sanitiser that this class
|
|
31838
|
+
* does not cover reds -- the failure mode a hand list has and a derivation
|
|
31839
|
+
* does not.
|
|
31840
|
+
*/
|
|
31841
|
+
const SECRET_SCAN_INVISIBLES = /[\p{Cc}\p{Cf}\p{Me}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/gu;
|
|
31842
|
+
/**
|
|
31843
|
+
* The one string space in which this module tests for, masks, and prints a
|
|
31844
|
+
* possibly-secret-bearing name.
|
|
31845
|
+
*
|
|
31846
|
+
* `.trim()` mirrors `displaySafe`, whose trim this replaces on these paths. It
|
|
31847
|
+
* is applied to the TEXT only -- see {@link canonicalNeedle}, where trimming
|
|
31848
|
+
* would silently shorten a recorded secret.
|
|
31849
|
+
*/
|
|
31850
|
+
function canonicalForSecretScan(text) {
|
|
31851
|
+
return text.replace(SECRET_SCAN_INVISIBLES, "").trim();
|
|
31852
|
+
}
|
|
31853
|
+
/**
|
|
31854
|
+
* A recorded secret as a NEEDLE in canonical space.
|
|
31855
|
+
*
|
|
31856
|
+
* STRIPPED BUT NOT TRIMMED. That is not an oversight of the symmetry with
|
|
31857
|
+
* {@link canonicalForSecretScan} but the deliberate asymmetry between a
|
|
31858
|
+
* haystack and a needle: whitespace at the edge of a recorded secret is part
|
|
31859
|
+
* of the secret and sits in the MIDDLE of a longer key, so trimming the needle
|
|
31860
|
+
* stops it matching there. Measured -- a recorded `"ab "` embedded in
|
|
31861
|
+
* `x-ab -y` was masked before this change and came back `safe` after it.
|
|
31862
|
+
*
|
|
31863
|
+
* Canonicalising the needle at all is what let the raw-key fallback arm go
|
|
31864
|
+
* away: a secret whose own plaintext carries a stripped character used to
|
|
31865
|
+
* survive in the raw key and be destroyed in the sanitised one, so it could be
|
|
31866
|
+
* DETECTED only from the raw form and MASKED in neither -- a permanent
|
|
31867
|
+
* `withheld`. Here it is both found and masked.
|
|
31868
|
+
*/
|
|
31869
|
+
function canonicalNeedle(plaintext) {
|
|
31870
|
+
return plaintext.replace(SECRET_SCAN_INVISIBLES, "");
|
|
31871
|
+
}
|
|
31872
|
+
/**
|
|
31873
|
+
* The recorded secrets as NEEDLES, keyed by their canonical form.
|
|
31874
|
+
*
|
|
31875
|
+
* A needle whose canonical form is EMPTY is dropped: canonicalisation can turn
|
|
31876
|
+
* a non-empty recorded value into `''`, and `maskEveryOccurrence` splitting on
|
|
31877
|
+
* `''` interleaves the mask between every character of the key. The resolver
|
|
31878
|
+
* never records an empty secret, so nothing upstream guards this. Measured
|
|
31879
|
+
* without the guard: `OrdinaryKey` came back as
|
|
31880
|
+
* `O***r***d***i***n***a***r***y***K***e***y`.
|
|
31881
|
+
*/
|
|
31882
|
+
function canonicalNeedles(secrets) {
|
|
31883
|
+
const out = /* @__PURE__ */ new Map();
|
|
31884
|
+
for (const [plaintext, expression] of secrets ?? []) {
|
|
31885
|
+
const needle = canonicalNeedle(plaintext);
|
|
31886
|
+
if (needle.length > 0) out.set(needle, expression);
|
|
31887
|
+
}
|
|
31888
|
+
return out;
|
|
31889
|
+
}
|
|
31890
|
+
/**
|
|
31140
31891
|
* Which recorded secrets are visible in `text`, or `undefined` when none is.
|
|
31141
31892
|
*
|
|
31142
31893
|
* ONE rule for both callers below, because they were inconsistent and the
|
|
@@ -31161,8 +31912,14 @@ const MIN_SECRET_NEEDLE = 4;
|
|
|
31161
31912
|
*/
|
|
31162
31913
|
function secretsPresentIn(text, secrets) {
|
|
31163
31914
|
if (!secrets || secrets.size === 0) return void 0;
|
|
31915
|
+
const haystack = canonicalForSecretScan(text);
|
|
31164
31916
|
const exposure = /* @__PURE__ */ new Map();
|
|
31165
|
-
for (const [plaintext, expression] of secrets)
|
|
31917
|
+
for (const [plaintext, expression] of secrets) {
|
|
31918
|
+
const needle = canonicalNeedle(plaintext);
|
|
31919
|
+
const canonicalHit = haystack === needle || needle.length >= MIN_SECRET_NEEDLE && haystack.includes(needle);
|
|
31920
|
+
const rawHit = text === plaintext || plaintext.length >= MIN_SECRET_NEEDLE && text.includes(plaintext);
|
|
31921
|
+
if (canonicalHit || rawHit) exposure.set(plaintext, expression);
|
|
31922
|
+
}
|
|
31166
31923
|
return exposure.size > 0 ? exposure : void 0;
|
|
31167
31924
|
}
|
|
31168
31925
|
/**
|
|
@@ -31252,10 +32009,13 @@ function maskEveryOccurrence(text, exposure) {
|
|
|
31252
32009
|
* unchanged. stderr is a reader like any other, so the invariant is absolute: a
|
|
31253
32010
|
* message must never claim a masking it did not perform.
|
|
31254
32011
|
*/
|
|
31255
|
-
function secretBearingExportNameWarning(outputKey, exportName, exposure) {
|
|
31256
|
-
const
|
|
31257
|
-
const
|
|
31258
|
-
|
|
32012
|
+
function secretBearingExportNameWarning(outputKey, exportName, exposure, secrets) {
|
|
32013
|
+
const corpus = secrets ?? exposure;
|
|
32014
|
+
const name = secretSafeKeyDisplay(exportName, corpus, exposure);
|
|
32015
|
+
const shown = name.kind === "masked" ? `(masked: "${name.text}") ` : "";
|
|
32016
|
+
const ownerForceMask = /* @__PURE__ */ new Map();
|
|
32017
|
+
for (const [plaintext, expression] of exposure) if (plaintext === outputKey || canonicalNeedle(plaintext) === canonicalForSecretScan(outputKey) || plaintext.length >= MIN_SECRET_NEEDLE) ownerForceMask.set(plaintext, expression);
|
|
32018
|
+
return `Output ${displayTextOrWithheld(secretSafeKeyDisplay(outputKey, corpus, ownerForceMask))} 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.`;
|
|
31259
32019
|
}
|
|
31260
32020
|
/**
|
|
31261
32021
|
* Test `key` for recorded secret plaintext and return how it may be shown.
|
|
@@ -31265,60 +32025,62 @@ function secretBearingExportNameWarning(outputKey, exportName, exposure) {
|
|
|
31265
32025
|
* key safe to print" would disagree on the boundary cases those two encode
|
|
31266
32026
|
* (the whole-key match for a sub-floor needle, longest-needle-first masking).
|
|
31267
32027
|
*
|
|
31268
|
-
*
|
|
31269
|
-
*
|
|
31270
|
-
*
|
|
31271
|
-
*
|
|
31272
|
-
*
|
|
31273
|
-
*
|
|
31274
|
-
*
|
|
31275
|
-
*
|
|
31276
|
-
*
|
|
31277
|
-
*
|
|
31278
|
-
|
|
31279
|
-
|
|
31280
|
-
|
|
31281
|
-
|
|
31282
|
-
|
|
31283
|
-
|
|
31284
|
-
|
|
31285
|
-
|
|
31286
|
-
|
|
31287
|
-
|
|
31288
|
-
|
|
31289
|
-
|
|
31290
|
-
|
|
31291
|
-
|
|
31292
|
-
|
|
31293
|
-
* DELETES while `displaySafe` replaces with a space, so strip-then-display
|
|
31294
|
-
* yields `"ab"` and display-then-strip yields `"a b"`. Stripping first keeps a
|
|
31295
|
-
* name carrying them from being padded out.
|
|
31296
|
-
*
|
|
31297
|
-
* `displaySafe` also `.trim()`s, which `stripControlChars` alone did not: a
|
|
31298
|
-
* display-shape change for a key with leading or trailing whitespace. Stated
|
|
31299
|
-
* because it is a real difference, not hidden.
|
|
31300
|
-
*
|
|
31301
|
-
* The sibling warnings in this file still use `stripControlChars` ALONE and
|
|
31302
|
-
* carry the `U+2028` half of the gap; widening that helper, or converting
|
|
31303
|
-
* them, changes call sites this issue does not touch, so it is filed as issue
|
|
31304
|
-
* [#2874](https://github.com/go-to-k/cdkd/issues/2874) rather than done here.
|
|
31305
|
-
*/
|
|
31306
|
-
function secretSafeKeyDisplay(key, secrets) {
|
|
31307
|
-
const sanitise = (text) => displaySafe(stripControlChars(text));
|
|
31308
|
-
const shown = sanitise(key);
|
|
31309
|
-
const exposure = stateKeySecretExposure(shown, secrets) ?? stateKeySecretExposure(key, secrets);
|
|
31310
|
-
if (!exposure) return {
|
|
32028
|
+
* CANONICAL SPACE, not a composition of the two sanitisers. An earlier
|
|
32029
|
+
* revision of this comment described running `stripControlChars` and then
|
|
32030
|
+
* `displaySafe`, with a table of what each one touches and a note that the
|
|
32031
|
+
* ORDER was load-bearing. That composition WAS the defect (issue
|
|
32032
|
+
* [#2874](https://github.com/go-to-k/cdkd/issues/2874)): it leaves three
|
|
32033
|
+
* strings in play -- the raw key the verdict came from, the sanitised key that
|
|
32034
|
+
* was printed, and the masked one between them -- and `stripControlChars`
|
|
32035
|
+
* DELETES, so a plaintext split by one of its characters is absent from the
|
|
32036
|
+
* first and contiguous in the second. See {@link SECRET_SCAN_INVISIBLES} for
|
|
32037
|
+
* the class that replaced it and why it is derived rather than enumerated.
|
|
32038
|
+
*/
|
|
32039
|
+
function secretSafeKeyDisplay(key, secrets, forceMask) {
|
|
32040
|
+
const shown = canonicalForSecretScan(key);
|
|
32041
|
+
const exposure = stateKeySecretExposure(key, secrets);
|
|
32042
|
+
const mask = canonicalNeedles(forceMask);
|
|
32043
|
+
for (const [plaintext, expression] of exposure ?? []) {
|
|
32044
|
+
const needle = canonicalNeedle(plaintext);
|
|
32045
|
+
if (needle.length > 0) mask.set(needle, expression);
|
|
32046
|
+
}
|
|
32047
|
+
if (mask.size === 0) return {
|
|
32048
|
+
kind: "safe",
|
|
32049
|
+
text: shown
|
|
32050
|
+
};
|
|
32051
|
+
const masked = maskEveryOccurrence(shown, mask);
|
|
32052
|
+
if (masked === shown) return exposure ? { kind: "withheld" } : {
|
|
31311
32053
|
kind: "safe",
|
|
31312
32054
|
text: shown
|
|
31313
32055
|
};
|
|
31314
|
-
|
|
31315
|
-
if (masked === shown) return { kind: "withheld" };
|
|
32056
|
+
if (stateKeySecretExposure(masked, secrets)) return { kind: "withheld" };
|
|
31316
32057
|
return {
|
|
31317
32058
|
kind: "masked",
|
|
31318
32059
|
text: masked
|
|
31319
32060
|
};
|
|
31320
32061
|
}
|
|
31321
32062
|
/**
|
|
32063
|
+
* The display for a name plus the verdict its CALLER needs, as one value.
|
|
32064
|
+
*
|
|
32065
|
+
* Exists so a caller cannot take the verdict from one call and the text from
|
|
32066
|
+
* another — the shape of the bug in {@link secretSafeKeyDisplay}'s own callers
|
|
32067
|
+
* (issue #2874), one level up.
|
|
32068
|
+
*/
|
|
32069
|
+
function secretBearing(display) {
|
|
32070
|
+
return display.kind !== "safe";
|
|
32071
|
+
}
|
|
32072
|
+
/**
|
|
32073
|
+
* What a message prints in place of a name it may not show.
|
|
32074
|
+
*
|
|
32075
|
+
* Deliberately not name-shaped and never quoted as if it were a key: a reader
|
|
32076
|
+
* has to be able to tell this is the tool declining, not an odd export name.
|
|
32077
|
+
*/
|
|
32078
|
+
const WITHHELD_NAME_DISPLAY = "<name withheld: contains a secret>";
|
|
32079
|
+
/** The text of a display, or {@link WITHHELD_NAME_DISPLAY} when there is none. */
|
|
32080
|
+
function displayTextOrWithheld(display) {
|
|
32081
|
+
return display.kind === "withheld" ? WITHHELD_NAME_DISPLAY : display.text;
|
|
32082
|
+
}
|
|
32083
|
+
/**
|
|
31322
32084
|
* Warning for a state KEY that already holds secret plaintext — the residue an
|
|
31323
32085
|
* EARLIER binary left when it published an export name that resolved to one.
|
|
31324
32086
|
*
|
|
@@ -31329,8 +32091,9 @@ function secretSafeKeyDisplay(key, secrets) {
|
|
|
31329
32091
|
* redeploy, which rewrites `state.outputs` wholesale and republishes the index.
|
|
31330
32092
|
* Reported so the `--dry-run --fail` CI gate stops calling such a state clean.
|
|
31331
32093
|
*/
|
|
31332
|
-
function secretBearingStateKeyWarning(stackName,
|
|
31333
|
-
|
|
32094
|
+
function secretBearingStateKeyWarning(stackName, display) {
|
|
32095
|
+
const clause = display.kind === "masked" ? `(masked: "${display.text}") ` : `(the name is withheld: masking it would leave the secret readable) `;
|
|
32096
|
+
return `State for ${canonicalForSecretScan(stackName)} holds an output KEY that renders a secret ${clause}— 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.`;
|
|
31334
32097
|
}
|
|
31335
32098
|
/**
|
|
31336
32099
|
* Warning for an `Export.Name` colliding with an output NAME it does not own.
|
|
@@ -31339,13 +32102,18 @@ function secretBearingStateKeyWarning(stackName, key, exposure) {
|
|
|
31339
32102
|
* says which value survives — the export is skipped, so the key keeps the
|
|
31340
32103
|
* output's own value.
|
|
31341
32104
|
*
|
|
31342
|
-
*
|
|
31343
|
-
*
|
|
31344
|
-
*
|
|
31345
|
-
|
|
31346
|
-
|
|
31347
|
-
|
|
31348
|
-
|
|
32105
|
+
* MASKED HERE, and the bound that used to excuse it is named rather than
|
|
32106
|
+
* relied on. This message prints a name that MATCHED a declared output name --
|
|
32107
|
+
* template text -- and a secret-bearing `Export.Name` is refused by
|
|
32108
|
+
* {@link secretBearingExportNameWarning} upstream. But that is SOMEBODY ELSE'S
|
|
32109
|
+
* VERDICT, and this site is reached from exactly the arm taken when it MISSED:
|
|
32110
|
+
* before issue [#2874](https://github.com/go-to-k/cdkd/issues/2874)
|
|
32111
|
+
* canonicalised the containment scan, it missed for eight of ten invisible
|
|
32112
|
+
* characters, and this message printed the plaintext verbatim.
|
|
32113
|
+
*/
|
|
32114
|
+
function exportAliasCollisionWarning(outputKey, exportName, secrets) {
|
|
32115
|
+
const shown = displayTextOrWithheld(secretSafeKeyDisplay(exportName, secrets));
|
|
32116
|
+
const from = displayTextOrWithheld(secretSafeKeyDisplay(outputKey, secrets));
|
|
31349
32117
|
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.`;
|
|
31350
32118
|
}
|
|
31351
32119
|
/**
|
|
@@ -31360,12 +32128,12 @@ function exportAliasCollisionWarning(outputKey, exportName) {
|
|
|
31360
32128
|
* {@link collectDeclaredOutputNames}.
|
|
31361
32129
|
*/
|
|
31362
32130
|
function exportAliasCollisionScrubWarning(outputKey, exportName, secrets) {
|
|
31363
|
-
const
|
|
31364
|
-
|
|
31365
|
-
|
|
31366
|
-
}
|
|
31367
|
-
const
|
|
31368
|
-
return `Output ${
|
|
32131
|
+
const nameDisplay = (name) => secretSafeKeyDisplay(name, secrets);
|
|
32132
|
+
const exportDisplay = nameDisplay(exportName);
|
|
32133
|
+
const shown = displayTextOrWithheld(exportDisplay);
|
|
32134
|
+
const storedUnder = exportDisplay.kind === "withheld" ? "the stored value under that name" : `the stored value under "${shown}"`;
|
|
32135
|
+
const ownerDisplay = nameDisplay(outputKey);
|
|
32136
|
+
return `Output ${ownerDisplay.kind === "withheld" && exportDisplay.kind === "withheld" ? "<the owning output, name withheld: contains a secret>" : displayTextOrWithheld(ownerDisplay)} exports as ${exportDisplay.kind === "withheld" ? shown : `"${shown}"`}, which is also the name of another output in this stack — state cannot say which of the two ${storedUnder} 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.`;
|
|
31369
32137
|
}
|
|
31370
32138
|
|
|
31371
32139
|
//#endregion
|
|
@@ -36010,7 +36778,7 @@ var DeployEngine = class {
|
|
|
36010
36778
|
const next = { ...op };
|
|
36011
36779
|
if (next.properties) next.properties = redactSecretsForState(next.properties, ownSecrets, templateProps);
|
|
36012
36780
|
if (next.attemptedProperties) next.attemptedProperties = redactSecretsForState(markSameGenerationBag({ ...next.attemptedProperties }), ownSecrets, templateProps);
|
|
36013
|
-
if (next.previousState) next.previousState = scrubResourceRecord(next.previousState, ownSecrets);
|
|
36781
|
+
if (next.previousState) next.previousState = scrubResourceRecord(next.previousState, ownSecrets, void 0, STATE_SOURCED_READBACK_RULES);
|
|
36014
36782
|
return next;
|
|
36015
36783
|
});
|
|
36016
36784
|
}
|
|
@@ -36322,7 +37090,7 @@ var DeployEngine = class {
|
|
|
36322
37090
|
this.logger.info("No changes detected. Stack is up to date.");
|
|
36323
37091
|
let persistedOutputs = currentState.outputs ?? {};
|
|
36324
37092
|
if (!this.options.dryRun) {
|
|
36325
|
-
const resolvedOutputs = this.redactOutputs(await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, outputsDigestSource, parameterValues, conditions));
|
|
37093
|
+
const resolvedOutputs = this.redactOutputs(await withSharedDrainBudget(() => this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, outputsDigestSource, parameterValues, conditions)));
|
|
36326
37094
|
const resolutionFailed = Object.values(resolvedOutputs).some((v) => v === void 0);
|
|
36327
37095
|
const outputsChanged = !resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs);
|
|
36328
37096
|
const currentEffectiveExports = new Set(importableOutputKeys(currentState));
|
|
@@ -36667,7 +37435,7 @@ var DeployEngine = class {
|
|
|
36667
37435
|
}
|
|
36668
37436
|
let outputs;
|
|
36669
37437
|
try {
|
|
36670
|
-
outputs = await this.resolveOutputs(template, newResources, stackName, outputsDigestSource, parameterValues, conditions);
|
|
37438
|
+
outputs = await withSharedDrainBudget(() => this.resolveOutputs(template, newResources, stackName, outputsDigestSource, parameterValues, conditions));
|
|
36671
37439
|
const resolvedOutputsBeforeRedaction = outputs;
|
|
36672
37440
|
outputs = this.redactOutputs(outputs);
|
|
36673
37441
|
this.rememberRecoverableMaskedOutputs(stackName, resolvedOutputsBeforeRedaction, outputs);
|
|
@@ -36938,6 +37706,8 @@ var DeployEngine = class {
|
|
|
36938
37706
|
renderer.removeTask(logicalId);
|
|
36939
37707
|
const message = error instanceof Error ? error.message : String(error);
|
|
36940
37708
|
this.logger.error(this.maskForResource(logicalId, `Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`));
|
|
37709
|
+
const orphanAdvice = this.orphanedNameCollisionAdvice(change.changeType, logicalId, error);
|
|
37710
|
+
if (orphanAdvice) this.logger.error(this.maskForResource(logicalId, orphanAdvice));
|
|
36941
37711
|
this.recordEvent({
|
|
36942
37712
|
eventType: "RESOURCE_FAILED",
|
|
36943
37713
|
stackName,
|
|
@@ -37630,6 +38400,99 @@ var DeployEngine = class {
|
|
|
37630
38400
|
};
|
|
37631
38401
|
}
|
|
37632
38402
|
/**
|
|
38403
|
+
* The plain-CREATE sibling of {@link replacementNameOrigin} (issue
|
|
38404
|
+
* [#2902](https://github.com/go-to-k/cdkd/issues/2902)).
|
|
38405
|
+
*
|
|
38406
|
+
* A CREATE that collides on a name cdkd DERIVED is very likely a resource
|
|
38407
|
+
* cdkd itself left behind: `DeletionPolicy: Retain` makes a rollback drop
|
|
38408
|
+
* the state record while leaving the resource in AWS (CloudFormation
|
|
38409
|
+
* semantics, and deliberate), and cdkd's generated names carry no random
|
|
38410
|
+
* component (`generateResourceName`) — so the next deploy asks AWS for
|
|
38411
|
+
* exactly the name the orphan still holds, fails, rolls back again, and
|
|
38412
|
+
* repeats forever. Before this, the user saw only the bare AWS sentence:
|
|
38413
|
+
* nothing named the collision's cause and nothing named a way out, so the
|
|
38414
|
+
* reported recovery was hand-deleting resources through the AWS API.
|
|
38415
|
+
*
|
|
38416
|
+
* CloudFormation never shows this because its generated names carry a
|
|
38417
|
+
* random suffix, so a retained orphan cannot collide with a later deploy.
|
|
38418
|
+
* The breakage is that combination — CFn's retain semantics with cdkd's
|
|
38419
|
+
* deterministic naming — rather than either half, which is why the fix here
|
|
38420
|
+
* is a diagnosis and a remedy rather than a behaviour change. Whether cdkd
|
|
38421
|
+
* should instead RE-ADOPT the retained resource is issue
|
|
38422
|
+
* [#2914](https://github.com/go-to-k/cdkd/issues/2914).
|
|
38423
|
+
*
|
|
38424
|
+
* Returns `undefined` — leaving the pre-existing wording untouched — for
|
|
38425
|
+
* every case it cannot vouch for:
|
|
38426
|
+
*
|
|
38427
|
+
* - not a CREATE. A replacement collision DOES arrive here — the
|
|
38428
|
+
* `NAMED_REPLACEMENT_COLLISION` throws happen inside `provisionResourceBody`,
|
|
38429
|
+
* which the caller invokes inside the same `try`, and this method's own
|
|
38430
|
+
* suite asserts their line was logged. What refuses them is the
|
|
38431
|
+
* `ProvisioningError` check below (they throw `CdkdError`), so deleting
|
|
38432
|
+
* EITHER guard alone leaves the suite green. That does not make this one
|
|
38433
|
+
* dead: a non-CREATE `ProvisioningError` whose message carries
|
|
38434
|
+
* `already exists` — an UPDATE-path sub-resource conflict — would reach
|
|
38435
|
+
* the advice without it, and the replacement message's remedy is to
|
|
38436
|
+
* RENAME, which does not recover an orphan. (An earlier revision of this
|
|
38437
|
+
* comment claimed the throws "never reach this catch at all". Three
|
|
38438
|
+
* reviewers disproved it independently.);
|
|
38439
|
+
* - not a name collision;
|
|
38440
|
+
* - no physical id on the error (a create that failed BEFORE the AWS call
|
|
38441
|
+
* never names one). At RUNTIME this is subsumed by the next guard --
|
|
38442
|
+
* `looksLikeCdkdGeneratedName` refuses a falsy id on its own first line,
|
|
38443
|
+
* measured: deleting this check ALONE leaves the suite green, deleting
|
|
38444
|
+
* both together reds it. It stays for the TYPE narrowing the message
|
|
38445
|
+
* interpolation needs, and so the refusal is readable here rather than
|
|
38446
|
+
* inferred from another module;
|
|
38447
|
+
* - a name cdkd did not derive — a user-supplied name may collide with a
|
|
38448
|
+
* resource of someone else's entirely, and telling that user to
|
|
38449
|
+
* `cdkd import` it would be advice to adopt what this stack does not own;
|
|
38450
|
+
* - a NESTED-STACK child. Its stack name is `<parent>~<logicalId>`, and CDK's
|
|
38451
|
+
* own stack-name rule bars `~`, so no Cloud Assembly stack can ever carry
|
|
38452
|
+
* it — `cdkd import` resolves its target from the assembly and walks
|
|
38453
|
+
* top-level stacks only, so the command would be unrunnable. That is the
|
|
38454
|
+
* same #2610 class this method's `canImport` check exists for, one level
|
|
38455
|
+
* down, so the child takes the delete-only arm.
|
|
38456
|
+
*
|
|
38457
|
+
* **A cdkd-DERIVED name is not proof the resource is THIS stack's**, which
|
|
38458
|
+
* the first revision of this advice assumed. Two ways it is not, both
|
|
38459
|
+
* reachable: a globally-namespaced type (`AWS::S3::Bucket` is the documented
|
|
38460
|
+
* exception — see `.claude/rules/provider-resource-identity.md`) can collide
|
|
38461
|
+
* with ANOTHER ACCOUNT's resource, and because the derivation is predictable
|
|
38462
|
+
* that name can be pre-registered by someone else; and the same stack name
|
|
38463
|
+
* deployed in two REGIONS derives the same name for a global type, so the
|
|
38464
|
+
* collision is with a live resource another state file already owns —
|
|
38465
|
+
* importing it would give two stacks one resource, and either `cdkd destroy`
|
|
38466
|
+
* would then delete it out from under the other. So the message names the
|
|
38467
|
+
* orphan as the LIKELY case rather than the certain one, and asks the reader
|
|
38468
|
+
* to confirm ownership before adopting.
|
|
38469
|
+
*/
|
|
38470
|
+
orphanedNameCollisionAdvice(changeType, logicalId, error) {
|
|
38471
|
+
if (changeType !== "CREATE") return void 0;
|
|
38472
|
+
if (!(error instanceof ProvisioningError)) return void 0;
|
|
38473
|
+
const physicalId = error.physicalId;
|
|
38474
|
+
if (!physicalId) return void 0;
|
|
38475
|
+
if (!isNameCollisionError(error.message)) return void 0;
|
|
38476
|
+
const stackName = getCurrentStackName();
|
|
38477
|
+
if (!looksLikeCdkdGeneratedName(physicalId, logicalId, stackName)) return void 0;
|
|
38478
|
+
if (!stackName) return void 0;
|
|
38479
|
+
const safeId = displaySafe(physicalId, { asciiOnly: true });
|
|
38480
|
+
const safeStack = displaySafe(stackName, { asciiOnly: true });
|
|
38481
|
+
const safeLogicalId = displaySafe(logicalId, { asciiOnly: true });
|
|
38482
|
+
const diagnosis = `${safeLogicalId}: the name AWS reports as taken (${safeId}) is one cdkd DERIVED from the logical id, and that derivation has no random component — so this is most likely a resource an earlier cdkd run left behind. A rollback leaves a resource carrying DeletionPolicy: Retain in AWS and drops it from state, as CloudFormation does; what differs is the name. CloudFormation would generate a fresh one for an unnamed resource and redeploy clean, whereas cdkd asks again for the name the orphan still holds — so re-running does not clear this.`;
|
|
38483
|
+
const deleteArm = `If it is not a resource you want to keep, delete ${safeId} in AWS — after confirming it holds nothing you need, since Retain is what kept it — and re-deploy.`;
|
|
38484
|
+
let canImport;
|
|
38485
|
+
try {
|
|
38486
|
+
canImport = typeof this.providerRegistry.getProvider(error.resourceType).import === "function";
|
|
38487
|
+
} catch {
|
|
38488
|
+
canImport = false;
|
|
38489
|
+
}
|
|
38490
|
+
const commandNamesTheRightResource = safeId === physicalId && safeStack === stackName && safeLogicalId === logicalId;
|
|
38491
|
+
const importableTarget = !stackName.includes("~");
|
|
38492
|
+
if (!canImport || !commandNamesTheRightResource || !importableTarget) return `${diagnosis} ${!canImport ? `cdkd cannot adopt ${displaySafe(error.resourceType, { asciiOnly: true })} back into state (its provider implements no import)` : !importableTarget ? `this is a nested-stack child, whose stack name cdkd import cannot resolve` : `cdkd cannot render an import command that provably names this resource`}, so the way forward is to delete it. ${deleteArm}`;
|
|
38493
|
+
return `${diagnosis} To recover, adopt it back into state instead of re-creating it: cdkd import ${shellQuote(safeStack)} --resource ${shellQuote(`${safeLogicalId}=${safeId}`)} (a selective import merges into existing state and needs no --force while the resource is absent from it). CONFIRM IT IS YOURS FIRST — a name cdkd derives is predictable, so for a globally-namespaced type it can belong to another account, and the same stack deployed in another region derives the same name. ${deleteArm}`;
|
|
38494
|
+
}
|
|
38495
|
+
/**
|
|
37633
38496
|
* Read `DeletionPolicy` / `UpdateReplacePolicy` from the synth template
|
|
37634
38497
|
* so they can be persisted in `ResourceState` (schema v5+). Always returns
|
|
37635
38498
|
* both keys (`undefined` when the template does not carry the attribute)
|
|
@@ -37865,9 +38728,16 @@ var DeployEngine = class {
|
|
|
37865
38728
|
* opposite sides of the same question. `secrets` is the outputs pass's own
|
|
37866
38729
|
* map: everything recorded before this handler runs, an `Export.Name`
|
|
37867
38730
|
* resolution's entries included (its `finally` merges them back before the
|
|
37868
|
-
* `catch` reaches here).
|
|
37869
|
-
*
|
|
37870
|
-
*
|
|
38731
|
+
* `catch` reaches here). Since issue #2563 a still-pending concurrent part
|
|
38732
|
+
* is in the PASS bag before this handler runs: the resolver drains every
|
|
38733
|
+
* part it started before a rejection reaches a caller. Not
|
|
38734
|
+
* unconditionally, and the weaker claim is the true one -- the drain is
|
|
38735
|
+
* bounded, and since the outputs pass wraps BOTH its loops in one budget
|
|
38736
|
+
* the bound spans the whole pass rather than one resolution: an early
|
|
38737
|
+
* failing output can leave a later `Export.Name` drain with no wait at all,
|
|
38738
|
+
* so a late record needs only a leg that had not finished recording by
|
|
38739
|
+
* then rather than one that outlived a full cap. (`inheritedSecrets` is the parent's, and no
|
|
38740
|
+
* resolution writes to it.)
|
|
37871
38741
|
*
|
|
37872
38742
|
* The strict arm's `cause` is masked as an OBJECT, through
|
|
37873
38743
|
* `maskSecretsInError` — a clone of each `Error` link `errorCauseChain`
|
|
@@ -37952,8 +38822,8 @@ var DeployEngine = class {
|
|
|
37952
38822
|
}
|
|
37953
38823
|
if (typeof exportName !== "string") continue;
|
|
37954
38824
|
const exposure = exportNameSecretExposure(exportName, nameSecrets, context.recordedSecretValues);
|
|
37955
|
-
if (exposure) this.logger.warn(secretBearingExportNameWarning(outputKey, exportName, exposure));
|
|
37956
|
-
else if (isExportAliasCollision(exportName, outputKey, publishedOutputNames)) this.logger.warn(exportAliasCollisionWarning(outputKey, exportName));
|
|
38825
|
+
if (exposure) this.logger.warn(secretBearingExportNameWarning(outputKey, exportName, exposure, context.recordedSecretValues));
|
|
38826
|
+
else if (isExportAliasCollision(exportName, outputKey, publishedOutputNames)) this.logger.warn(exportAliasCollisionWarning(outputKey, exportName, outputsPassSecrets));
|
|
37957
38827
|
else {
|
|
37958
38828
|
outputs[exportName] = value;
|
|
37959
38829
|
if (!this.resolvedExportNames.includes(exportName)) this.resolvedExportNames.push(exportName);
|
|
@@ -37987,5 +38857,5 @@ var DeployEngine = class {
|
|
|
37987
38857
|
};
|
|
37988
38858
|
|
|
37989
38859
|
//#endregion
|
|
37990
|
-
export { ProviderRegistry as $,
|
|
37991
|
-
//# sourceMappingURL=deploy-engine-
|
|
38860
|
+
export { ProviderRegistry as $, formatDockerLoginError as $n, PartialFailureError as $r, STATE_SOURCED_BASELINE_RULES as $t, renderStatefulReason as A, stringifyValue as An, PARTITION_TABLE as Ar, configStringRefusal as At, red as B, getBootstrapMarkerKey as Bn, setAwsClients as Br, s3BucketDomainName as Bt, refusesFinalSnapshot as C, CUSTOM_RESOURCE_RESPONSE_PREFIX as Cn, CFN_TEMPLATE_URL_LIMIT as Cr, refStateLookupFromResource as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, importableOutputs as Dn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as Dr, assertRegionMatch as Dt, extractDeploymentEventError as E, importableOutputKeys as En, uploadCfnTemplate as Er, resolveExplicitPhysicalId as Et, formatResourceLine as F, rewriteTemplateAssetReferences as Fn, clearBucketRegionCache as Fr, requireConfigString as Ft, isExportAliasCollision as G, validateContainerRepoName as Gn, DependencyError as Gr, DiffCalculator as Gt, collectDeclaredOutputNames as H, parseBootstrapMarker as Hn, CdkdError as Hr, s3BucketRegionalDomainName as Ht, bold as I, AssetModeResolver as In, resolveBucketRegion as Ir, classifyReplaySecretRegion as It, secretSafeKeyDisplay as J, buildDockerImage as Jn, IntrinsicResolutionRefusalError as Jr, findSilentDropProperties as Jt, secretBearing as K, buildDenyExternalAccessPolicy as Kn, DeployCancelledError as Kr, INTRINSIC_KEYS as Kt, cyan as L, BOOTSTRAP_MARKER_PREFIX as Ln, AwsClients as Lr, producerRegionsFromState as Lt, coerceWarmThroughput as M, buildAssetRedirectMap as Mn, derivePartitionAndUrlSuffix as Mr, replayWarn as Mt, isWarmThroughputDecrease as N, createAssetRedirectResolver as Nn, AssemblyReader as Nr, requireConfigArray as Nt, isStatefulRecreateTargetForReplace as O, shouldRetainResource as On, displaySafe as Or, coerceCfnBoolean as Ot, toFiniteNumber as P, loadPublishableAssetManifest as Pn, processStackMessages as Pr, requireConfigObject as Pt, clearOnUpdateRemoval as Q, dockerSpawnEnvWithSensitive as Qn, NestedStackChildDirectDestroyError as Qr, TemplateParser as Qt, gray as R, assertAssetBucketRegion as Rn, getAwsClients as Rr, withSharedDrainBudget as Rt, isFinalSnapshotError as S, shellQuote as Sn, CFN_TEMPLATE_BODY_LIMIT as Sr, parameterTypeMayLoseSecretIdentity as St, makeCanonicalizePropertiesFn as T, exportNamesCarriedFrom as Tn, findLargeInlineResources as Tr, normalizeAwsTagsToCfn as Tt, collectPublishedOutputNames as U, readBootstrapMarkerBody as Un, ConfigError as Ur, s3BucketWebsiteUrl as Ut, yellow as V, isCrossRegionRedirect as Vn, AssetError as Vr, s3BucketDualStackDomainName as Vt, exportAliasCollisionScrubWarning as W, validateAssetBucketName as Wn, CrossAccountSecretRefusalError as Wr, applyRoleArnIfSet as Wt, IAMRoleProvider as X, describeDockerExecFailure as Xn, LocalStartServiceError as Xr, withRetry as Xt, getCurrentResourceSecrets as Y, describeDockerCapturedOutput as Yn, LocalInvokeBuildError as Yr, describeTypeWithThrottleRetry as Yt, collectInlinePolicyNamesManagedBySiblings as Z, describeDockerFailure as Zn, LockError as Zr, DagBuilder as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, retryClassificationText as _i, rebuildClientForBucketRegion as _n, resolveStateBucketWithDefault as _r, carriesDynamicReference as _t, DeploymentEventsStore as a, StateError as ai, dynamicReferenceTokens as an, escapeRegExp$1 as ar, endCommandInterruptScope as at, ccRoutedFinalSnapshotError as b, buildLockContentionMessage as bn, stateBucketExistenceConfirmed as br, getAccountInfo as bt, replayFailedOperations as c, isCdkdError as ci, isSingleDynamicReferenceToken as cn, getDockerImageBySourceHash as cr, startInterruptWatch as ct, updatePartialReason as d, isMarkedNonRetryable as di, recordMaskOnlyValue as dn, getDefaultStateBucketName as dr, UNSPECIFIED_SKIP_REASON as dt, ProvisioningError as ei, STATE_SOURCED_CROSS_GENERATION_RULES as en, getDockerCmd as er, wouldReturnToSdkProvider as et, withResourceDeadline as f, isRetryableTransientError as fi, recoverMaskedOutput as fn, getLegacyStateBucketName as fr, deleteIndeterminateGuards as ft, bindingSkippedOutputs as g, markRedactedCause as gi, S3StateBackend as gn, resolveSkipPrefix as gr, IntrinsicFunctionResolver as gt, computeImplicitDeleteEdges as h, markNonRetryable as hi, LockManager as hn, resolveCaptureObservedState as hr, isTerminationProtectionPropagationError as ht, DeploymentEventsReader as i, StackTerminationProtectionError as ii, createSecretMasker as in, runDockerStreaming as ir, beginCommandInterruptScope as it, WARM_THROUGHPUT_MEMBERS as j, WorkGraph as jn, canonicalizeRegion as jr, readConfigString as jt, isStatefulRecreateTargetSync as k, AssetPublisher as kn, expectedOwnerParam as kr, configBooleanRefusal as kt, replayRollback as l, normalizeAwsError as li, maskSecretsInError as ln, Synthesizer as lr, CloudControlProvider as lt, IMPLICIT_DELETE_DEPENDENCIES as m, isTransientServerError as mi, scrubResourceRecord as mn, resolveAutoAssetStorage as mr, disableInstanceApiTermination as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, ResourceUpdateNotSupportedError as ni, TEMPLATE_SOURCED_RULES as nn, redactDockerArgvValues as nr, maskDeep as nt, planFailedOps as o, SynthesisError as oi, errorCauseChain as on, stripControlChars as or, interruptWatchListenerCount as ot, maskingRetryLogger as p, isThrottlingError as pi, redactSecretsForState as pn, resolveApp as pr, deleteSkipReason as pt, secretBearingStateKeyWarning as q, describeAwsFailure as qn, DynamicReferenceRegionAmbiguousError as qr, findActionableSilentDrops as qt, DeployEngine as r, StackHasActiveImportsError as ri, carriesSecretMask as rn, runDockerForeground as rr, maskerOrIdentity as rt, planRollback as s, formatError as si, identityKeyFor as sn, AssetManifestLoader as sr, isInterruptedWaitError as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, ResourceTimeoutError as ti, STATE_SOURCED_READBACK_RULES as tn, partitionSensitiveEnv as tr, createMaskedRetryLogger as tt, updatePartialMessage as u, withErrorHandling as ui, maskSecretsInText as un, synthesisStatusMessage as ur, slowCcOperationTimeoutMs as ut, PRE_DELETE_SNAPSHOT_TYPES as v, __exportAll as vi, UNRENDERABLE as vn, resolveStateBucketWithDefaultAndSource as vr, cfnRefValueFromPhysicalId as vt, unsupportedFinalSnapshotError as w, DEFAULT_STATE_PREFIX as wn, MIGRATE_TMP_PREFIX as wr, WAFv2WebACLProvider as wt, createPreDeleteFinalSnapshot as x, forceQuitRecoveryClause as xn, warnDeprecatedNoPrefixCliFlag as xr, isUnboundTemplateParameter as xt, buildFinalSnapshotIdentifier as y, buildForceUnlockCommand as yn, resolveUseCdkBootstrapAssets as yr, coerceParameterTypedValue as yt, green as z, ensureAssetStorage as zn, resetAwsClients as zr, s3BucketArn as zt };
|
|
38861
|
+
//# sourceMappingURL=deploy-engine-Dsf9bTqH.js.map
|