@go-to-k/cdkd 0.288.3 → 0.288.4
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-TexsJufw.js} +2 -2
- package/dist/{asg-provider-CQrHElrl.js.map → asg-provider-TexsJufw.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-CfxcC3q3.js → deploy-engine-BiPxTgKT.js} +515 -80
- package/dist/deploy-engine-BiPxTgKT.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-BXH3FeeV.js → program-Ci4Y5UO9.js} +92 -85
- package/dist/{program-BXH3FeeV.js.map → program-Ci4Y5UO9.js.map} +1 -1
- package/dist/{version-CEyZvTcB.js → version-BmM4QJ9t.js} +2 -2
- package/dist/{version-CEyZvTcB.js.map → version-BmM4QJ9t.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-BmM4QJ9t.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";
|
|
@@ -4017,6 +4017,18 @@ async function expectedOwnerParam(client) {
|
|
|
4017
4017
|
* A caller whose value has a KNOWN ASCII charset (a stack name, an AWS region)
|
|
4018
4018
|
* should pass `asciiOnly`, which is a positive allowlist and therefore has no
|
|
4019
4019
|
* such residual at all.
|
|
4020
|
+
*
|
|
4021
|
+
* ONE CALLER DELIBERATELY GOES WIDER, and it is recorded here so an editor of
|
|
4022
|
+
* the residual note above knows a second module now disagrees with it.
|
|
4023
|
+
* `src/deployment/outputs-export-alias.ts` deletes a class derived from
|
|
4024
|
+
* `\p{Cc}` / `\p{Cf}` / `\p{Zl}` / `\p{Zp}` /
|
|
4025
|
+
* `\p{Default_Ignorable_Code_Point}`, because on THAT path the subject is a possibly
|
|
4026
|
+
* secret-bearing name in an operator's log: a plaintext split by a zero-width
|
|
4027
|
+
* character is READ as if it were contiguous, so it is disclosed without any
|
|
4028
|
+
* paste, and the command-forgery reasoning above does not transfer (issue
|
|
4029
|
+
* [#2874](https://github.com/go-to-k/cdkd/issues/2874)). Nothing here changes
|
|
4030
|
+
* -- widening this helper would alter every caller that merely wants a
|
|
4031
|
+
* terminal-safe string.
|
|
4020
4032
|
*/
|
|
4021
4033
|
function displaySafe(value, opts) {
|
|
4022
4034
|
if (value === void 0 || value === null) return "";
|
|
@@ -19910,6 +19922,113 @@ function s3BucketWebsiteUrl(bucketName, region) {
|
|
|
19910
19922
|
return `http://${bucketName}.s3-website${S3_WEBSITE_ENDPOINT_LEGACY_DASH_REGIONS.has(folded) ? "-" : "."}${folded}.${urlSuffix}`;
|
|
19911
19923
|
}
|
|
19912
19924
|
|
|
19925
|
+
//#endregion
|
|
19926
|
+
//#region src/deployment/drain-budget.ts
|
|
19927
|
+
/**
|
|
19928
|
+
* The drain BUDGET for one intrinsic resolution (issue
|
|
19929
|
+
* [#2563](https://github.com/go-to-k/cdkd/issues/2563)).
|
|
19930
|
+
*
|
|
19931
|
+
* Its own module rather than a corner of `intrinsic-function-resolver.ts`,
|
|
19932
|
+
* and that is forced rather than tidy: 70+ unit files `vi.mock` the resolver
|
|
19933
|
+
* module, and the ones that REPLACE its exports rather than spreading
|
|
19934
|
+
* `...actual` see no new export -- so an opener declared there, whether as a
|
|
19935
|
+
* resolver method or as a module-level function, reddens most of
|
|
19936
|
+
* `tests/unit/deployment` the moment `deploy-engine.ts` imports it (measured,
|
|
19937
|
+
* both ways). A module the engine and the resolver both import, and nobody
|
|
19938
|
+
* mocks, is the shape that works.
|
|
19939
|
+
*/
|
|
19940
|
+
/**
|
|
19941
|
+
* Named `drainDeadlines` from when it held one, and kept because the name is
|
|
19942
|
+
* cited from several comments and a rename is churn this issue does not need.
|
|
19943
|
+
* It holds a {@link DrainBudget} -- remaining WAIT, not an instant.
|
|
19944
|
+
*/
|
|
19945
|
+
const drainDeadlines = new AsyncLocalStorage();
|
|
19946
|
+
/**
|
|
19947
|
+
* Run `fn` under ONE drain budget (issue #2563), inheriting the caller's if
|
|
19948
|
+
* there is one.
|
|
19949
|
+
*
|
|
19950
|
+
* A module-level function rather than a resolver METHOD, deliberately: the
|
|
19951
|
+
* engine's callers mock the resolver wholesale, and a new method on that
|
|
19952
|
+
* surface would make every such mock throw before it resolved anything
|
|
19953
|
+
* (measured -- most of `tests/unit/deployment` red).
|
|
19954
|
+
*
|
|
19955
|
+
* Why a caller needs this. `resolve` opens a budget per CALL, and a caller
|
|
19956
|
+
* that resolves in a LOOP therefore gets one cap per iteration: the outputs
|
|
19957
|
+
* pass walks `template.Outputs` sequentially, so the DRAIN WAIT it could
|
|
19958
|
+
* spend before `saveState`, with the S3 lock held, was `#outputs x` the cap
|
|
19959
|
+
* rather than the cap. CloudFormation allows 200 outputs. Wrapping the loop
|
|
19960
|
+
* here bounds the total drain WAIT under it at one budget.
|
|
19961
|
+
*
|
|
19962
|
+
* The budget is REMAINING milliseconds of wait, not a deadline, and that
|
|
19963
|
+
* distinction is the whole of it: an absolute deadline is spent by wall
|
|
19964
|
+
* clock, so ordinary resolution time between drains burns it although
|
|
19965
|
+
* nothing drained -- an output failing with a 5 ms sibling would leave a
|
|
19966
|
+
* later one with zero grace after 60 s of clean AWS work. Charged only while
|
|
19967
|
+
* a drain is actually waiting, and only once for nested drains whose waits
|
|
19968
|
+
* overlap, an iteration that spent nothing keeps its full grace.
|
|
19969
|
+
*
|
|
19970
|
+
* WHAT IT DOES NOT BOUND, since the cap is not a deadline on the pass: the
|
|
19971
|
+
* cap is armed by a REJECTION and bounds only the extra wait a drain takes
|
|
19972
|
+
* after one. Ordinary resolution time is outside it entirely -- a lookup
|
|
19973
|
+
* that hangs with no sibling rejection anywhere is unbounded here exactly
|
|
19974
|
+
* as it was before this issue, and `withResourceDeadline` does not reach
|
|
19975
|
+
* the outputs pass. So this wrap bounds aggregate drain grace, not the
|
|
19976
|
+
* hold.
|
|
19977
|
+
*
|
|
19978
|
+
* THE TRADE, stated because it is real: iterations that actually WAIT spend
|
|
19979
|
+
* the shared budget, so a later one can find it exhausted and its drain get
|
|
19980
|
+
* no grace. That is the same exposure issue
|
|
19981
|
+
* [#2814](https://github.com/go-to-k/cdkd/issues/2814) records for nesting,
|
|
19982
|
+
* now reachable across a loop as well -- and on the `Export.Name` leg the
|
|
19983
|
+
* cost is a DROPPED write rather than a late one, since that block's
|
|
19984
|
+
* `nameSecrets` is a per-iteration local. What bounds it is that only real
|
|
19985
|
+
* waiting spends the budget: reaching zero takes a full cap of drain wait
|
|
19986
|
+
* inside one pass, not merely a slow pass. It is taken deliberately -- an
|
|
19987
|
+
* unbounded hold on a deploy's state save costs a first deploy every
|
|
19988
|
+
* resource it just created -- and the caller chooses, since a loop that
|
|
19989
|
+
* would rather buy grace per iteration simply does not wrap.
|
|
19990
|
+
*
|
|
19991
|
+
* `evaluateConditions` deliberately does NOT wrap its loop: a failed
|
|
19992
|
+
* condition is downgraded and evaluation continues, it runs before any
|
|
19993
|
+
* resource is provisioned, and one condition's slow parts should
|
|
19994
|
+
* not spend the next one's budget. Its aggregate is `#conditions x` the cap,
|
|
19995
|
+
* with the deploy lock already held -- less severe than the outputs pass,
|
|
19996
|
+
* where the state save is the thing waiting, but not lock-free.
|
|
19997
|
+
*
|
|
19998
|
+
* The other resolve LOOPS in the tree, assessed rather than assumed -- and
|
|
19999
|
+
* the first version of this note got two of them wrong, so each is stated
|
|
20000
|
+
* with what was checked:
|
|
20001
|
+
*
|
|
20002
|
+
* - `cdkd scrub` (`scrub.ts`): lock acquired above, `saveState` downstream,
|
|
20003
|
+
* and its resolve loops -- the resources loop, two output loops, and
|
|
20004
|
+
* `resolveCrossStackReads`, a per-leaf loop the other three each invoke.
|
|
20005
|
+
* One budget is hoisted over all of them. That wrap is INLINE rather than
|
|
20006
|
+
* around a callee, so the callee-keyed table in
|
|
20007
|
+
* `intrinsic-resolver-concurrent-drain.test.ts` cannot see it; an
|
|
20008
|
+
* owner-keyed case in that same file fences it instead.
|
|
20009
|
+
* - `cdkd import` (`import.ts`): `acquireLock` at the root and per child,
|
|
20010
|
+
* `resolveImportedProperties` loops the resources, `saveState` follows.
|
|
20011
|
+
* WRAPPED at both call sites.
|
|
20012
|
+
* - `cdkd export` (`export.ts`): child locks acquired before
|
|
20013
|
+
* `buildResolvedParametersPerStack`, which loops `resolve` over the
|
|
20014
|
+
* intrinsic parameters. WRAPPED.
|
|
20015
|
+
* - `diff-recursive.ts`: loops `resolve` over a parent's parameters with no
|
|
20016
|
+
* lock and no state write -- a read-only diff. LEFT per-call, since a
|
|
20017
|
+
* shared budget would buy no safety and would spend one resolution's
|
|
20018
|
+
* grace on the next.
|
|
20019
|
+
*
|
|
20020
|
+
* The drain is what issue #2563 adds, so the wait in all four is new and
|
|
20021
|
+
* bounding it where it can hold something is part of adding it.
|
|
20022
|
+
*/
|
|
20023
|
+
async function withSharedDrainBudget(fn) {
|
|
20024
|
+
if (drainDeadlines.getStore() !== void 0) return await fn();
|
|
20025
|
+
return await drainDeadlines.run({
|
|
20026
|
+
remaining: void 0,
|
|
20027
|
+
waiting: 0,
|
|
20028
|
+
since: void 0
|
|
20029
|
+
}, fn);
|
|
20030
|
+
}
|
|
20031
|
+
|
|
19913
20032
|
//#endregion
|
|
19914
20033
|
//#region src/deployment/secret-region-classification.ts
|
|
19915
20034
|
/**
|
|
@@ -22133,6 +22252,170 @@ const MAX_LISTED_AVAILABLE_OUTPUTS = 10;
|
|
|
22133
22252
|
*/
|
|
22134
22253
|
const dynamicReferenceRetryDelays = {};
|
|
22135
22254
|
/**
|
|
22255
|
+
* How long {@link allSettledKeepingFirstRejection} waits for the remaining
|
|
22256
|
+
* parts AFTER a rejection is in hand. Double the largest FIXED wait in this
|
|
22257
|
+
* file (the `Fn::GetAtt` `Ipv6CidrBlocks` poll's sleep budget, 15 attempts
|
|
22258
|
+
* x 2 s), which makes it a hang guard rather than a schedule — not a
|
|
22259
|
+
* guarantee that healthy work fits inside it; see the function's own note.
|
|
22260
|
+
*
|
|
22261
|
+
* It is the budget for one CALL of {@link IntrinsicFunctionResolver.resolve}
|
|
22262
|
+
* and everything nested under it: nested drains share the REMAINING wait, so
|
|
22263
|
+
* a template's nesting depth cannot multiply it. It is NOT a bound on a caller
|
|
22264
|
+
* that resolves in a LOOP -- each iteration opens its own budget unless the
|
|
22265
|
+
* caller wraps the loop in {@link withSharedDrainBudget}, which the outputs
|
|
22266
|
+
* pass does and `evaluateConditions` deliberately does not (its aggregate is
|
|
22267
|
+
* `#conditions x` this, before any resource is provisioned but with the
|
|
22268
|
+
* deploy lock already held).
|
|
22269
|
+
*/
|
|
22270
|
+
const DRAIN_AFTER_REJECTION_MS = 6e4;
|
|
22271
|
+
/** Sentinel for "the cap expired", distinguishable from any resolved value. */
|
|
22272
|
+
const CAP_EXPIRED = Symbol("drain-cap-expired");
|
|
22273
|
+
/**
|
|
22274
|
+
* Test seam: overriding `ms` lets a unit test drive the cap without a real
|
|
22275
|
+
* minute of waiting (mirrors {@link dynamicReferenceRetryDelays}).
|
|
22276
|
+
*/
|
|
22277
|
+
const concurrentDrainCap = {};
|
|
22278
|
+
/**
|
|
22279
|
+
* `Promise.all`'s RESULT and its choice of error, with `Promise.allSettled`'s
|
|
22280
|
+
* TIMING: every promise started here has settled before this returns, and
|
|
22281
|
+
* before it throws unless the cap below expires first (issue
|
|
22282
|
+
* [#2563](https://github.com/go-to-k/cdkd/issues/2563)).
|
|
22283
|
+
*
|
|
22284
|
+
* Why the resolver needs that. Resolving a secret dynamic reference RECORDS
|
|
22285
|
+
* `plaintext -> expression` into `context.recordedSecretValues` just before
|
|
22286
|
+
* its promise settles, and that map is what every masking and redaction site
|
|
22287
|
+
* downstream uses as its needle set. Under a bare `Promise.all` a rejecting
|
|
22288
|
+
* part surfaces IMMEDIATELY, so a caller's `catch` / `finally` can run while a
|
|
22289
|
+
* sibling part is still in flight: `DeployEngine`'s `Export.Name` block copies
|
|
22290
|
+
* its private map into the pass map in exactly such a `finally`, and the
|
|
22291
|
+
* sibling's recording then lands in the private map after the copy and reaches
|
|
22292
|
+
* nothing. Draining here fixes it for every caller at once, which a
|
|
22293
|
+
* consumer-side drain cannot — `cdkd scrub`'s shared-map view (issue
|
|
22294
|
+
* [#2531](https://github.com/go-to-k/cdkd/issues/2531)) lets a late write land
|
|
22295
|
+
* whenever it happens but still cannot make it land before the next consumer
|
|
22296
|
+
* runs.
|
|
22297
|
+
*
|
|
22298
|
+
* THE ERROR IS SELECTED BY TIME, NOT BY INPUT ORDER, which is what `Promise.all`
|
|
22299
|
+
* does and what a naive `Promise.allSettled` + "first rejected entry" would
|
|
22300
|
+
* silently change: with two parts rejecting out of input order, the entry scan
|
|
22301
|
+
* reports the LATER one. Each promise gets its own `catch`, so the callbacks
|
|
22302
|
+
* fire in rejection order and the first assignment wins.
|
|
22303
|
+
*
|
|
22304
|
+
* Every input is `catch`-ed, so nothing here can raise an unhandled rejection
|
|
22305
|
+
* while the drain waits.
|
|
22306
|
+
*
|
|
22307
|
+
* THE WAIT IS CAPPED ONCE A REJECTION IS IN HAND. The drain exists to let a
|
|
22308
|
+
* sibling finish RECORDING, and that is worth a wait — but `resolveOutputs`
|
|
22309
|
+
* runs at `deploy-engine.ts`'s worst moment: after every resource has been
|
|
22310
|
+
* created in AWS, before the final `saveState`, with the S3 lock held and its
|
|
22311
|
+
* heartbeat pushing `expiresAt` forward. An unbounded wait there costs a deploy
|
|
22312
|
+
* its state and its lock, and on a FIRST deploy (`currentEtag` undefined, so
|
|
22313
|
+
* the incremental saves were no-ops) every created resource becomes invisible
|
|
22314
|
+
* to cdkd. Nothing else bounds it: `withRetry` caps ATTEMPTS not duration, no
|
|
22315
|
+
* `requestTimeout` is configured, and `withResourceDeadline` wraps
|
|
22316
|
+
* `provisionResourceBody` — which does bound the resource path, property
|
|
22317
|
+
* resolution included, but not the outputs pass. So once a rejection is
|
|
22318
|
+
* recorded the remaining settles race {@link DRAIN_AFTER_REJECTION_MS}, and the
|
|
22319
|
+
* recorded rejection is thrown when it expires.
|
|
22320
|
+
*
|
|
22321
|
+
* The cap is a HANG GUARD, and it does not claim to be more. It is sized
|
|
22322
|
+
* against the largest fixed wait in this file — the `Fn::GetAtt`
|
|
22323
|
+
* `Ipv6CidrBlocks` poll's sleep budget, 15 attempts x 2 s, about 30 s — but
|
|
22324
|
+
* that budget is a floor, not a ceiling: the poll also awaits 15 AWS calls,
|
|
22325
|
+
* and one part can drive several lookups in sequence through object
|
|
22326
|
+
* properties or `Fn::Sub` variables. Two healthy shapes measured on review
|
|
22327
|
+
* already exceed 60 s — three sequential `Ipv6CidrBlocks` polls in one part
|
|
22328
|
+
* is 3 x (15 x 2 s) = 90 s with no hang and no throttling, and five throttled
|
|
22329
|
+
* dynamic references is 5 x (1+2+4+8 s) = 75 s at
|
|
22330
|
+
* `MAX_DYNAMIC_REFERENCE_THROTTLE_RETRIES` = 4. Healthy work CAN therefore
|
|
22331
|
+
* outlast the cap, and when it does its recording lands after the rejection
|
|
22332
|
+
* was released, which is the window this whole function exists to close.
|
|
22333
|
+
* Nothing here cancels that sibling — it keeps running and can record
|
|
22334
|
+
* arbitrarily later — so what the cap bounds is the WAIT, not the lateness.
|
|
22335
|
+
* That is the trade taken deliberately: a bounded wait with a late record
|
|
22336
|
+
* still possible beyond it, against an unbounded hold on a deploy's state
|
|
22337
|
+
* save.
|
|
22338
|
+
*
|
|
22339
|
+
* AND A DRAIN CAN GET NO GRACE AT ALL. The budget is shared REMAINING wait,
|
|
22340
|
+
* so a drain that arms once it is spent gets
|
|
22341
|
+
* `Math.max(0, remaining - openWindow)` = 0 and releases its rejection on the
|
|
22342
|
+
* next macrotask. That applies to any drain NOT ALREADY ARMED when a rejection
|
|
22343
|
+
* reaches it: the ordinary case is an inner drain that spends the full budget
|
|
22344
|
+
* and throws, whose every not-yet-armed ancestor then arms against an
|
|
22345
|
+
* exhausted remaining-wait budget. What it does NOT mean is that a fast sibling is exposed: a
|
|
22346
|
+
* sibling still pending at that moment has itself been running at least the
|
|
22347
|
+
* budget. What it means is that the sibling's own RECORDING gets no wait --
|
|
22348
|
+
* a lookup begun late inside a long-running part is fast in itself and still
|
|
22349
|
+
* lands after the rejection was released. That reasoning covers the NESTED
|
|
22350
|
+
* ancestor case; a caller that wraps a LOOP widens it, because a later
|
|
22351
|
+
* iteration starts FRESH siblings against a budget an earlier one already
|
|
22352
|
+
* spent and those need not be long-running at all. So the exposure does not
|
|
22353
|
+
* require the "healthy work slower than 60 s" shape the sizing paragraph
|
|
22354
|
+
* describes; that shape is the cheapest way to reach it with no nesting and
|
|
22355
|
+
* no wrapped loop, not the floor.
|
|
22356
|
+
* Residual: issue
|
|
22357
|
+
* [#2814](https://github.com/go-to-k/cdkd/issues/2814).
|
|
22358
|
+
*
|
|
22359
|
+
* ONE REMAINING CONSEQUENCE, deliberate and pinned by a case rather than only
|
|
22360
|
+
* described: the earliest-in-time rule holds PER INVOCATION, not across
|
|
22361
|
+
* NESTED resolutions. For a join whose parts are `[listWithAnEarlyFailure,
|
|
22362
|
+
* laterFailure]`, the inner list's drain holds its own rejection while its
|
|
22363
|
+
* slow sibling finishes, so the outer join captures the later failure first
|
|
22364
|
+
* and reports that instead. That is not only cosmetic: the retry classifiers
|
|
22365
|
+
* DO read the message (`retryClassificationText` feeds
|
|
22366
|
+
* `isRetryableTransientError`, whose `RETRYABLE_ERROR_MESSAGE_PATTERNS` is an
|
|
22367
|
+
* explicit substring table), so swapping which failure surfaces can swap a
|
|
22368
|
+
* transient verdict for a terminal one. What does not change is that the
|
|
22369
|
+
* resolution FAILS: both are genuine failures of the same resolve, and the
|
|
22370
|
+
* selection was already timing-dependent — `Promise.all` reports whichever
|
|
22371
|
+
* lost the race. The drain adds a systematic bias toward the SHALLOWER
|
|
22372
|
+
* failure where the old race was arbitrary. Residual: issue
|
|
22373
|
+
* [#2805](https://github.com/go-to-k/cdkd/issues/2805).
|
|
22374
|
+
*/
|
|
22375
|
+
async function allSettledKeepingFirstRejection(promises) {
|
|
22376
|
+
let rejection;
|
|
22377
|
+
const shared = drainDeadlines.getStore();
|
|
22378
|
+
let armCap;
|
|
22379
|
+
const guarded = promises.map((promise) => promise.catch((error) => {
|
|
22380
|
+
if (rejection === void 0) {
|
|
22381
|
+
rejection = { error };
|
|
22382
|
+
armCap?.();
|
|
22383
|
+
}
|
|
22384
|
+
}));
|
|
22385
|
+
let capTimer;
|
|
22386
|
+
let charged = false;
|
|
22387
|
+
const capped = new Promise((resolve) => {
|
|
22388
|
+
armCap = () => {
|
|
22389
|
+
const budget = concurrentDrainCap.ms ?? DRAIN_AFTER_REJECTION_MS;
|
|
22390
|
+
let wait = budget;
|
|
22391
|
+
if (shared !== void 0) {
|
|
22392
|
+
shared.remaining ??= budget;
|
|
22393
|
+
const openWindow = shared.since === void 0 ? 0 : Date.now() - shared.since;
|
|
22394
|
+
wait = Math.max(0, shared.remaining - openWindow);
|
|
22395
|
+
if (shared.waiting === 0) shared.since = Date.now();
|
|
22396
|
+
shared.waiting += 1;
|
|
22397
|
+
charged = true;
|
|
22398
|
+
}
|
|
22399
|
+
capTimer = setTimeout(() => resolve(CAP_EXPIRED), wait);
|
|
22400
|
+
};
|
|
22401
|
+
});
|
|
22402
|
+
try {
|
|
22403
|
+
const outcome = await Promise.race([Promise.all(guarded), capped]);
|
|
22404
|
+
if (rejection !== void 0) throw rejection.error;
|
|
22405
|
+
if (outcome === CAP_EXPIRED) throw markNonRetryable(/* @__PURE__ */ new Error("drain cap expired with no rejection recorded"));
|
|
22406
|
+
return outcome;
|
|
22407
|
+
} finally {
|
|
22408
|
+
if (capTimer !== void 0) clearTimeout(capTimer);
|
|
22409
|
+
if (charged && shared !== void 0) {
|
|
22410
|
+
shared.waiting -= 1;
|
|
22411
|
+
if (shared.waiting === 0 && shared.since !== void 0) {
|
|
22412
|
+
shared.remaining = Math.max(0, (shared.remaining ?? 0) - (Date.now() - shared.since));
|
|
22413
|
+
shared.since = void 0;
|
|
22414
|
+
}
|
|
22415
|
+
}
|
|
22416
|
+
}
|
|
22417
|
+
}
|
|
22418
|
+
/**
|
|
22136
22419
|
* Is `region` safe to build an AWS SDK client from?
|
|
22137
22420
|
*
|
|
22138
22421
|
* This is a SECURITY gate, not an AWS region registry, and the distinction
|
|
@@ -23035,7 +23318,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23035
23318
|
* Resolve all intrinsic functions in a value
|
|
23036
23319
|
*/
|
|
23037
23320
|
async resolve(value, context) {
|
|
23038
|
-
return await this.resolveValue(value, context);
|
|
23321
|
+
return await withSharedDrainBudget(() => this.resolveValue(value, context));
|
|
23039
23322
|
}
|
|
23040
23323
|
/**
|
|
23041
23324
|
* Evaluate all conditions in the template
|
|
@@ -23077,7 +23360,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23077
23360
|
}
|
|
23078
23361
|
};
|
|
23079
23362
|
for (const name of Object.keys(templateConditions)) try {
|
|
23080
|
-
await evaluateByName(name);
|
|
23363
|
+
await withSharedDrainBudget(() => evaluateByName(name));
|
|
23081
23364
|
} catch (error) {
|
|
23082
23365
|
this.logger.warn(this.maskSecretsForLog(`Failed to evaluate condition ${name}: ${error instanceof Error ? error.message : String(error)}, assuming false`, maskingContext));
|
|
23083
23366
|
conditions[name] = false;
|
|
@@ -23093,7 +23376,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23093
23376
|
if (typeof value === "string" && value.includes("{{resolve:")) return await this.resolveDynamicReferences(value, context);
|
|
23094
23377
|
return value;
|
|
23095
23378
|
}
|
|
23096
|
-
if (Array.isArray(value)) return (await
|
|
23379
|
+
if (Array.isArray(value)) return (await allSettledKeepingFirstRejection(value.map((v) => this.resolveValue(v, context)))).filter((v) => v !== AWS_NO_VALUE);
|
|
23097
23380
|
const obj = value;
|
|
23098
23381
|
if ("Ref" in obj) return await this.resolveRef(obj["Ref"], context);
|
|
23099
23382
|
if ("Fn::GetAtt" in obj) return await this.resolveGetAtt(obj["Fn::GetAtt"], context);
|
|
@@ -23920,7 +24203,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23920
24203
|
let values = rawValues;
|
|
23921
24204
|
if (!Array.isArray(values)) values = await this.resolveValue(values, context);
|
|
23922
24205
|
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
|
|
24206
|
+
let result = (await allSettledKeepingFirstRejection(values.map(async (v) => {
|
|
23924
24207
|
const resolved = await this.resolveValue(v, context);
|
|
23925
24208
|
return String(resolved);
|
|
23926
24209
|
}))).join(delimiter);
|
|
@@ -26660,7 +26943,7 @@ var CloudControlProvider = class {
|
|
|
26660
26943
|
const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
|
|
26661
26944
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
26662
26945
|
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-
|
|
26946
|
+
const { ASGProvider } = await import("./asg-provider-TexsJufw.js").then((n) => n.n);
|
|
26664
26947
|
const asgProvider = new ASGProvider();
|
|
26665
26948
|
return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
|
|
26666
26949
|
}
|
|
@@ -31137,6 +31420,134 @@ function isExportAliasCollision(exportName, outputKey, ownedOutputNames) {
|
|
|
31137
31420
|
*/
|
|
31138
31421
|
const MIN_SECRET_NEEDLE = 4;
|
|
31139
31422
|
/**
|
|
31423
|
+
* Characters deleted before any secret containment test on this path, and
|
|
31424
|
+
* deleted from the text that gets PRINTED — ONE class, because the verdict and
|
|
31425
|
+
* the printed text have to live in the same string space (issue
|
|
31426
|
+
* [#2874](https://github.com/go-to-k/cdkd/issues/2874)).
|
|
31427
|
+
*
|
|
31428
|
+
* WHY A THIRD CLASS RATHER THAN EITHER SANITISER'S. `stripControlChars`
|
|
31429
|
+
* DELETES its class; `displaySafe` REPLACES its own with a space. Composing
|
|
31430
|
+
* them — which is what every site here used to do — leaves three different
|
|
31431
|
+
* strings in play: the raw key the verdict was taken from, the sanitised key
|
|
31432
|
+
* that was printed, and the masked one in between. A recorded secret split by
|
|
31433
|
+
* a DELETED character is absent from the raw key and contiguous in the printed
|
|
31434
|
+
* one, so the verdict said `safe` over text that held the plaintext; a secret
|
|
31435
|
+
* split by a REPLACED one is absent from both and prints one character short
|
|
31436
|
+
* of the plaintext, which a `not.toContain(SECRET)` assertion cannot see.
|
|
31437
|
+
* Measured across both classes: eight of ten characters reconstituted the
|
|
31438
|
+
* secret verbatim, and the remaining two printed it minus one character.
|
|
31439
|
+
*
|
|
31440
|
+
* So the fix is not another arm on the check — it is removing the second and
|
|
31441
|
+
* third string. Everything below happens in `canonicalForSecretScan` space.
|
|
31442
|
+
*
|
|
31443
|
+
* THE CLASS IS DERIVED FROM UNICODE CATEGORIES, NOT ENUMERATED. A hand list
|
|
31444
|
+
* was written first -- both sanitisers' classes plus the four residuals
|
|
31445
|
+
* `display-safe.ts` names -- and review measured it arbitrary: `U+2060`
|
|
31446
|
+
* (WORD JOINER) verdicted `safe` and printed visibly-contiguous plaintext
|
|
31447
|
+
* while `U+FEFF` was caught, and `U+2060` is the character Unicode itself
|
|
31448
|
+
* designates as the replacement for `U+FEFF` in that role. Eight more did the
|
|
31449
|
+
* same (`U+00AD`, `U+180E`, `U+FE0F`, `U+3164`, `U+2061`, `U+FFFB`, `U+115F`,
|
|
31450
|
+
* `U+17B4`). A list of the invisibles somebody happened to think of is not a
|
|
31451
|
+
* boundary; the general categories are.
|
|
31452
|
+
*
|
|
31453
|
+
* - `\p{Cc}` control and `\p{Cf}` format: the C0 / C1 and DEL ranges, the bidi
|
|
31454
|
+
* marks / embeddings / overrides / isolates, the zero-width set, `U+FEFF`,
|
|
31455
|
+
* `U+00AD`, `U+061C`, and the invisible-operator block.
|
|
31456
|
+
* - `\p{Zl}` / `\p{Zp}`: `U+2028` / `U+2029`, the two `displaySafe` REPLACES.
|
|
31457
|
+
* - `\p{Default_Ignorable_Code_Point}`: Unicode's own INTENT-TO-BE-IGNORED
|
|
31458
|
+
* property, which carries the variation selectors, the Mongolian free
|
|
31459
|
+
* variation selectors, the Hangul fillers and `U+034F` COMBINING GRAPHEME
|
|
31460
|
+
* JOINER.
|
|
31461
|
+
*
|
|
31462
|
+
* - `\p{Me}` ENCLOSING marks. The same zero-advance-width shape as `\p{Mn}`
|
|
31463
|
+
* below, and INCLUDED rather than deferred because the cost argument that
|
|
31464
|
+
* defers `\p{Mn}` does not transfer: `\p{Me}` is about a dozen code points
|
|
31465
|
+
* with no legitimate use in a resource name.
|
|
31466
|
+
*
|
|
31467
|
+
* NAMED RESIDUAL, because `\p{Default_Ignorable_Code_Point}` is Unicode's
|
|
31468
|
+
* INTENT-TO-BE-IGNORED property and NOT "everything that renders as nothing"
|
|
31469
|
+
* -- an earlier revision of this comment claimed the latter and review refuted
|
|
31470
|
+
* it. NONSPACING marks (`\p{Mn}`) carry zero advance width, so a secret split
|
|
31471
|
+
* by one renders contiguous while this class keeps it: measured with `U+09BC`,
|
|
31472
|
+
* which verdicts `safe` AND publishes the alias. Not widened here, because
|
|
31473
|
+
* `\p{Mn}` is the diacritics of Devanagari, Arabic, Hebrew and Vietnamese and
|
|
31474
|
+
* deleting it would mangle legitimate names for every user. Tracked as issue
|
|
31475
|
+
* [#2889](https://github.com/go-to-k/cdkd/issues/2889), which also carries the
|
|
31476
|
+
* homoglyph question. `origin/main` behaves identically, so this is a recorded
|
|
31477
|
+
* residual rather than something this change introduced.
|
|
31478
|
+
*
|
|
31479
|
+
* THE THIRD BULLET REPLACED A HAND TAIL, and the hand tail was measured
|
|
31480
|
+
* leaking. A first cut listed the ranges by hand -- `FE00-FE0F`,
|
|
31481
|
+
* `E0100-E01EF`, `180B-180D`, `115F`, `1160`, `3164`, `FFA0`, `17B4`, `17B5`
|
|
31482
|
+
* -- and review found `U+034F` outside it, printing `hunter2hunter2` in a
|
|
31483
|
+
* `warn` line under a `safe` verdict. It also spelled `180B-180D` while
|
|
31484
|
+
* calling itself "the Mongolian FVS", missing `U+180F`, which Unicode 14
|
|
31485
|
+
* added. Naming the PROPERTY is what makes the next such addition arrive
|
|
31486
|
+
* covered instead of arriving as another finding.
|
|
31487
|
+
*
|
|
31488
|
+
* WIDENED HERE AND NOT IN `display-safe.ts` because the two files answer
|
|
31489
|
+
* different questions. `displaySafe` is about a terminal rendering a value,
|
|
31490
|
+
* and its recorded reason for keeping these is command forgery -- where a
|
|
31491
|
+
* character that renders as nothing changes nothing. That reason does not
|
|
31492
|
+
* transfer to a secret: a plaintext split by an invisible character is READ by
|
|
31493
|
+
* a human exactly as if it were contiguous, so disclosure needs no paste.
|
|
31494
|
+
*
|
|
31495
|
+
* `tests/unit/deployment/secret-scan-class-superset.test.ts` fences this as a
|
|
31496
|
+
* SUPERSET of both sanitisers' classes by SCANNING CODE POINTS rather than by
|
|
31497
|
+
* comparing source text, so a future edit to either sanitiser that this class
|
|
31498
|
+
* does not cover reds -- the failure mode a hand list has and a derivation
|
|
31499
|
+
* does not.
|
|
31500
|
+
*/
|
|
31501
|
+
const SECRET_SCAN_INVISIBLES = /[\p{Cc}\p{Cf}\p{Me}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/gu;
|
|
31502
|
+
/**
|
|
31503
|
+
* The one string space in which this module tests for, masks, and prints a
|
|
31504
|
+
* possibly-secret-bearing name.
|
|
31505
|
+
*
|
|
31506
|
+
* `.trim()` mirrors `displaySafe`, whose trim this replaces on these paths. It
|
|
31507
|
+
* is applied to the TEXT only -- see {@link canonicalNeedle}, where trimming
|
|
31508
|
+
* would silently shorten a recorded secret.
|
|
31509
|
+
*/
|
|
31510
|
+
function canonicalForSecretScan(text) {
|
|
31511
|
+
return text.replace(SECRET_SCAN_INVISIBLES, "").trim();
|
|
31512
|
+
}
|
|
31513
|
+
/**
|
|
31514
|
+
* A recorded secret as a NEEDLE in canonical space.
|
|
31515
|
+
*
|
|
31516
|
+
* STRIPPED BUT NOT TRIMMED. That is not an oversight of the symmetry with
|
|
31517
|
+
* {@link canonicalForSecretScan} but the deliberate asymmetry between a
|
|
31518
|
+
* haystack and a needle: whitespace at the edge of a recorded secret is part
|
|
31519
|
+
* of the secret and sits in the MIDDLE of a longer key, so trimming the needle
|
|
31520
|
+
* stops it matching there. Measured -- a recorded `"ab "` embedded in
|
|
31521
|
+
* `x-ab -y` was masked before this change and came back `safe` after it.
|
|
31522
|
+
*
|
|
31523
|
+
* Canonicalising the needle at all is what let the raw-key fallback arm go
|
|
31524
|
+
* away: a secret whose own plaintext carries a stripped character used to
|
|
31525
|
+
* survive in the raw key and be destroyed in the sanitised one, so it could be
|
|
31526
|
+
* DETECTED only from the raw form and MASKED in neither -- a permanent
|
|
31527
|
+
* `withheld`. Here it is both found and masked.
|
|
31528
|
+
*/
|
|
31529
|
+
function canonicalNeedle(plaintext) {
|
|
31530
|
+
return plaintext.replace(SECRET_SCAN_INVISIBLES, "");
|
|
31531
|
+
}
|
|
31532
|
+
/**
|
|
31533
|
+
* The recorded secrets as NEEDLES, keyed by their canonical form.
|
|
31534
|
+
*
|
|
31535
|
+
* A needle whose canonical form is EMPTY is dropped: canonicalisation can turn
|
|
31536
|
+
* a non-empty recorded value into `''`, and `maskEveryOccurrence` splitting on
|
|
31537
|
+
* `''` interleaves the mask between every character of the key. The resolver
|
|
31538
|
+
* never records an empty secret, so nothing upstream guards this. Measured
|
|
31539
|
+
* without the guard: `OrdinaryKey` came back as
|
|
31540
|
+
* `O***r***d***i***n***a***r***y***K***e***y`.
|
|
31541
|
+
*/
|
|
31542
|
+
function canonicalNeedles(secrets) {
|
|
31543
|
+
const out = /* @__PURE__ */ new Map();
|
|
31544
|
+
for (const [plaintext, expression] of secrets ?? []) {
|
|
31545
|
+
const needle = canonicalNeedle(plaintext);
|
|
31546
|
+
if (needle.length > 0) out.set(needle, expression);
|
|
31547
|
+
}
|
|
31548
|
+
return out;
|
|
31549
|
+
}
|
|
31550
|
+
/**
|
|
31140
31551
|
* Which recorded secrets are visible in `text`, or `undefined` when none is.
|
|
31141
31552
|
*
|
|
31142
31553
|
* ONE rule for both callers below, because they were inconsistent and the
|
|
@@ -31161,8 +31572,14 @@ const MIN_SECRET_NEEDLE = 4;
|
|
|
31161
31572
|
*/
|
|
31162
31573
|
function secretsPresentIn(text, secrets) {
|
|
31163
31574
|
if (!secrets || secrets.size === 0) return void 0;
|
|
31575
|
+
const haystack = canonicalForSecretScan(text);
|
|
31164
31576
|
const exposure = /* @__PURE__ */ new Map();
|
|
31165
|
-
for (const [plaintext, expression] of secrets)
|
|
31577
|
+
for (const [plaintext, expression] of secrets) {
|
|
31578
|
+
const needle = canonicalNeedle(plaintext);
|
|
31579
|
+
const canonicalHit = haystack === needle || needle.length >= MIN_SECRET_NEEDLE && haystack.includes(needle);
|
|
31580
|
+
const rawHit = text === plaintext || plaintext.length >= MIN_SECRET_NEEDLE && text.includes(plaintext);
|
|
31581
|
+
if (canonicalHit || rawHit) exposure.set(plaintext, expression);
|
|
31582
|
+
}
|
|
31166
31583
|
return exposure.size > 0 ? exposure : void 0;
|
|
31167
31584
|
}
|
|
31168
31585
|
/**
|
|
@@ -31252,10 +31669,13 @@ function maskEveryOccurrence(text, exposure) {
|
|
|
31252
31669
|
* unchanged. stderr is a reader like any other, so the invariant is absolute: a
|
|
31253
31670
|
* message must never claim a masking it did not perform.
|
|
31254
31671
|
*/
|
|
31255
|
-
function secretBearingExportNameWarning(outputKey, exportName, exposure) {
|
|
31256
|
-
const
|
|
31257
|
-
const
|
|
31258
|
-
|
|
31672
|
+
function secretBearingExportNameWarning(outputKey, exportName, exposure, secrets) {
|
|
31673
|
+
const corpus = secrets ?? exposure;
|
|
31674
|
+
const name = secretSafeKeyDisplay(exportName, corpus, exposure);
|
|
31675
|
+
const shown = name.kind === "masked" ? `(masked: "${name.text}") ` : "";
|
|
31676
|
+
const ownerForceMask = /* @__PURE__ */ new Map();
|
|
31677
|
+
for (const [plaintext, expression] of exposure) if (plaintext === outputKey || canonicalNeedle(plaintext) === canonicalForSecretScan(outputKey) || plaintext.length >= MIN_SECRET_NEEDLE) ownerForceMask.set(plaintext, expression);
|
|
31678
|
+
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
31679
|
}
|
|
31260
31680
|
/**
|
|
31261
31681
|
* Test `key` for recorded secret plaintext and return how it may be shown.
|
|
@@ -31265,60 +31685,62 @@ function secretBearingExportNameWarning(outputKey, exportName, exposure) {
|
|
|
31265
31685
|
* key safe to print" would disagree on the boundary cases those two encode
|
|
31266
31686
|
* (the whole-key match for a sub-floor needle, longest-needle-first masking).
|
|
31267
31687
|
*
|
|
31268
|
-
*
|
|
31269
|
-
*
|
|
31270
|
-
*
|
|
31271
|
-
*
|
|
31272
|
-
*
|
|
31273
|
-
*
|
|
31274
|
-
*
|
|
31275
|
-
*
|
|
31276
|
-
*
|
|
31277
|
-
*
|
|
31278
|
-
|
|
31279
|
-
|
|
31280
|
-
|
|
31281
|
-
|
|
31282
|
-
|
|
31283
|
-
|
|
31284
|
-
|
|
31285
|
-
|
|
31286
|
-
|
|
31287
|
-
|
|
31288
|
-
* ORDER IS LOAD-BEARING, and for the OVERLAP set — not for the marks, which
|
|
31289
|
-
* an earlier revision of this comment named and measurement contradicted.
|
|
31290
|
-
* `displaySafe` never touches `U+200E` / `U+200F`, so those give identical
|
|
31291
|
-
* output either way. Where the two classes OVERLAP — `U+0000`-`U+001F`,
|
|
31292
|
-
* `U+007F`-`U+009F`, `U+202A`-`U+202E`, `U+2066`-`U+2069` — `stripControlChars`
|
|
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 {
|
|
31688
|
+
* CANONICAL SPACE, not a composition of the two sanitisers. An earlier
|
|
31689
|
+
* revision of this comment described running `stripControlChars` and then
|
|
31690
|
+
* `displaySafe`, with a table of what each one touches and a note that the
|
|
31691
|
+
* ORDER was load-bearing. That composition WAS the defect (issue
|
|
31692
|
+
* [#2874](https://github.com/go-to-k/cdkd/issues/2874)): it leaves three
|
|
31693
|
+
* strings in play -- the raw key the verdict came from, the sanitised key that
|
|
31694
|
+
* was printed, and the masked one between them -- and `stripControlChars`
|
|
31695
|
+
* DELETES, so a plaintext split by one of its characters is absent from the
|
|
31696
|
+
* first and contiguous in the second. See {@link SECRET_SCAN_INVISIBLES} for
|
|
31697
|
+
* the class that replaced it and why it is derived rather than enumerated.
|
|
31698
|
+
*/
|
|
31699
|
+
function secretSafeKeyDisplay(key, secrets, forceMask) {
|
|
31700
|
+
const shown = canonicalForSecretScan(key);
|
|
31701
|
+
const exposure = stateKeySecretExposure(key, secrets);
|
|
31702
|
+
const mask = canonicalNeedles(forceMask);
|
|
31703
|
+
for (const [plaintext, expression] of exposure ?? []) {
|
|
31704
|
+
const needle = canonicalNeedle(plaintext);
|
|
31705
|
+
if (needle.length > 0) mask.set(needle, expression);
|
|
31706
|
+
}
|
|
31707
|
+
if (mask.size === 0) return {
|
|
31311
31708
|
kind: "safe",
|
|
31312
31709
|
text: shown
|
|
31313
31710
|
};
|
|
31314
|
-
const masked =
|
|
31315
|
-
if (masked === shown) return { kind: "withheld" }
|
|
31711
|
+
const masked = maskEveryOccurrence(shown, mask);
|
|
31712
|
+
if (masked === shown) return exposure ? { kind: "withheld" } : {
|
|
31713
|
+
kind: "safe",
|
|
31714
|
+
text: shown
|
|
31715
|
+
};
|
|
31716
|
+
if (stateKeySecretExposure(masked, secrets)) return { kind: "withheld" };
|
|
31316
31717
|
return {
|
|
31317
31718
|
kind: "masked",
|
|
31318
31719
|
text: masked
|
|
31319
31720
|
};
|
|
31320
31721
|
}
|
|
31321
31722
|
/**
|
|
31723
|
+
* The display for a name plus the verdict its CALLER needs, as one value.
|
|
31724
|
+
*
|
|
31725
|
+
* Exists so a caller cannot take the verdict from one call and the text from
|
|
31726
|
+
* another — the shape of the bug in {@link secretSafeKeyDisplay}'s own callers
|
|
31727
|
+
* (issue #2874), one level up.
|
|
31728
|
+
*/
|
|
31729
|
+
function secretBearing(display) {
|
|
31730
|
+
return display.kind !== "safe";
|
|
31731
|
+
}
|
|
31732
|
+
/**
|
|
31733
|
+
* What a message prints in place of a name it may not show.
|
|
31734
|
+
*
|
|
31735
|
+
* Deliberately not name-shaped and never quoted as if it were a key: a reader
|
|
31736
|
+
* has to be able to tell this is the tool declining, not an odd export name.
|
|
31737
|
+
*/
|
|
31738
|
+
const WITHHELD_NAME_DISPLAY = "<name withheld: contains a secret>";
|
|
31739
|
+
/** The text of a display, or {@link WITHHELD_NAME_DISPLAY} when there is none. */
|
|
31740
|
+
function displayTextOrWithheld(display) {
|
|
31741
|
+
return display.kind === "withheld" ? WITHHELD_NAME_DISPLAY : display.text;
|
|
31742
|
+
}
|
|
31743
|
+
/**
|
|
31322
31744
|
* Warning for a state KEY that already holds secret plaintext — the residue an
|
|
31323
31745
|
* EARLIER binary left when it published an export name that resolved to one.
|
|
31324
31746
|
*
|
|
@@ -31329,8 +31751,9 @@ function secretSafeKeyDisplay(key, secrets) {
|
|
|
31329
31751
|
* redeploy, which rewrites `state.outputs` wholesale and republishes the index.
|
|
31330
31752
|
* Reported so the `--dry-run --fail` CI gate stops calling such a state clean.
|
|
31331
31753
|
*/
|
|
31332
|
-
function secretBearingStateKeyWarning(stackName,
|
|
31333
|
-
|
|
31754
|
+
function secretBearingStateKeyWarning(stackName, display) {
|
|
31755
|
+
const clause = display.kind === "masked" ? `(masked: "${display.text}") ` : `(the name is withheld: masking it would leave the secret readable) `;
|
|
31756
|
+
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
31757
|
}
|
|
31335
31758
|
/**
|
|
31336
31759
|
* Warning for an `Export.Name` colliding with an output NAME it does not own.
|
|
@@ -31339,13 +31762,18 @@ function secretBearingStateKeyWarning(stackName, key, exposure) {
|
|
|
31339
31762
|
* says which value survives — the export is skipped, so the key keeps the
|
|
31340
31763
|
* output's own value.
|
|
31341
31764
|
*
|
|
31342
|
-
*
|
|
31343
|
-
*
|
|
31344
|
-
*
|
|
31345
|
-
|
|
31346
|
-
|
|
31347
|
-
|
|
31348
|
-
|
|
31765
|
+
* MASKED HERE, and the bound that used to excuse it is named rather than
|
|
31766
|
+
* relied on. This message prints a name that MATCHED a declared output name --
|
|
31767
|
+
* template text -- and a secret-bearing `Export.Name` is refused by
|
|
31768
|
+
* {@link secretBearingExportNameWarning} upstream. But that is SOMEBODY ELSE'S
|
|
31769
|
+
* VERDICT, and this site is reached from exactly the arm taken when it MISSED:
|
|
31770
|
+
* before issue [#2874](https://github.com/go-to-k/cdkd/issues/2874)
|
|
31771
|
+
* canonicalised the containment scan, it missed for eight of ten invisible
|
|
31772
|
+
* characters, and this message printed the plaintext verbatim.
|
|
31773
|
+
*/
|
|
31774
|
+
function exportAliasCollisionWarning(outputKey, exportName, secrets) {
|
|
31775
|
+
const shown = displayTextOrWithheld(secretSafeKeyDisplay(exportName, secrets));
|
|
31776
|
+
const from = displayTextOrWithheld(secretSafeKeyDisplay(outputKey, secrets));
|
|
31349
31777
|
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
31778
|
}
|
|
31351
31779
|
/**
|
|
@@ -31360,12 +31788,12 @@ function exportAliasCollisionWarning(outputKey, exportName) {
|
|
|
31360
31788
|
* {@link collectDeclaredOutputNames}.
|
|
31361
31789
|
*/
|
|
31362
31790
|
function exportAliasCollisionScrubWarning(outputKey, exportName, secrets) {
|
|
31363
|
-
const
|
|
31364
|
-
|
|
31365
|
-
|
|
31366
|
-
}
|
|
31367
|
-
const
|
|
31368
|
-
return `Output ${
|
|
31791
|
+
const nameDisplay = (name) => secretSafeKeyDisplay(name, secrets);
|
|
31792
|
+
const exportDisplay = nameDisplay(exportName);
|
|
31793
|
+
const shown = displayTextOrWithheld(exportDisplay);
|
|
31794
|
+
const storedUnder = exportDisplay.kind === "withheld" ? "the stored value under that name" : `the stored value under "${shown}"`;
|
|
31795
|
+
const ownerDisplay = nameDisplay(outputKey);
|
|
31796
|
+
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
31797
|
}
|
|
31370
31798
|
|
|
31371
31799
|
//#endregion
|
|
@@ -36322,7 +36750,7 @@ var DeployEngine = class {
|
|
|
36322
36750
|
this.logger.info("No changes detected. Stack is up to date.");
|
|
36323
36751
|
let persistedOutputs = currentState.outputs ?? {};
|
|
36324
36752
|
if (!this.options.dryRun) {
|
|
36325
|
-
const resolvedOutputs = this.redactOutputs(await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, outputsDigestSource, parameterValues, conditions));
|
|
36753
|
+
const resolvedOutputs = this.redactOutputs(await withSharedDrainBudget(() => this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, outputsDigestSource, parameterValues, conditions)));
|
|
36326
36754
|
const resolutionFailed = Object.values(resolvedOutputs).some((v) => v === void 0);
|
|
36327
36755
|
const outputsChanged = !resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs);
|
|
36328
36756
|
const currentEffectiveExports = new Set(importableOutputKeys(currentState));
|
|
@@ -36667,7 +37095,7 @@ var DeployEngine = class {
|
|
|
36667
37095
|
}
|
|
36668
37096
|
let outputs;
|
|
36669
37097
|
try {
|
|
36670
|
-
outputs = await this.resolveOutputs(template, newResources, stackName, outputsDigestSource, parameterValues, conditions);
|
|
37098
|
+
outputs = await withSharedDrainBudget(() => this.resolveOutputs(template, newResources, stackName, outputsDigestSource, parameterValues, conditions));
|
|
36671
37099
|
const resolvedOutputsBeforeRedaction = outputs;
|
|
36672
37100
|
outputs = this.redactOutputs(outputs);
|
|
36673
37101
|
this.rememberRecoverableMaskedOutputs(stackName, resolvedOutputsBeforeRedaction, outputs);
|
|
@@ -37865,9 +38293,16 @@ var DeployEngine = class {
|
|
|
37865
38293
|
* opposite sides of the same question. `secrets` is the outputs pass's own
|
|
37866
38294
|
* map: everything recorded before this handler runs, an `Export.Name`
|
|
37867
38295
|
* resolution's entries included (its `finally` merges them back before the
|
|
37868
|
-
* `catch` reaches here).
|
|
37869
|
-
*
|
|
37870
|
-
*
|
|
38296
|
+
* `catch` reaches here). Since issue #2563 a still-pending concurrent part
|
|
38297
|
+
* is in the PASS bag before this handler runs: the resolver drains every
|
|
38298
|
+
* part it started before a rejection reaches a caller. Not
|
|
38299
|
+
* unconditionally, and the weaker claim is the true one -- the drain is
|
|
38300
|
+
* bounded, and since the outputs pass wraps BOTH its loops in one budget
|
|
38301
|
+
* the bound spans the whole pass rather than one resolution: an early
|
|
38302
|
+
* failing output can leave a later `Export.Name` drain with no wait at all,
|
|
38303
|
+
* so a late record needs only a leg that had not finished recording by
|
|
38304
|
+
* then rather than one that outlived a full cap. (`inheritedSecrets` is the parent's, and no
|
|
38305
|
+
* resolution writes to it.)
|
|
37871
38306
|
*
|
|
37872
38307
|
* The strict arm's `cause` is masked as an OBJECT, through
|
|
37873
38308
|
* `maskSecretsInError` — a clone of each `Error` link `errorCauseChain`
|
|
@@ -37952,8 +38387,8 @@ var DeployEngine = class {
|
|
|
37952
38387
|
}
|
|
37953
38388
|
if (typeof exportName !== "string") continue;
|
|
37954
38389
|
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));
|
|
38390
|
+
if (exposure) this.logger.warn(secretBearingExportNameWarning(outputKey, exportName, exposure, context.recordedSecretValues));
|
|
38391
|
+
else if (isExportAliasCollision(exportName, outputKey, publishedOutputNames)) this.logger.warn(exportAliasCollisionWarning(outputKey, exportName, outputsPassSecrets));
|
|
37957
38392
|
else {
|
|
37958
38393
|
outputs[exportName] = value;
|
|
37959
38394
|
if (!this.resolvedExportNames.includes(exportName)) this.resolvedExportNames.push(exportName);
|
|
@@ -37987,5 +38422,5 @@ var DeployEngine = class {
|
|
|
37987
38422
|
};
|
|
37988
38423
|
|
|
37989
38424
|
//#endregion
|
|
37990
|
-
export { ProviderRegistry as $,
|
|
37991
|
-
//# sourceMappingURL=deploy-engine-
|
|
38425
|
+
export { ProviderRegistry as $, partitionSensitiveEnv as $n, ResourceTimeoutError as $r, STATE_SOURCED_CROSS_GENERATION_RULES as $t, renderStatefulReason as A, buildAssetRedirectMap as An, derivePartitionAndUrlSuffix as Ar, configStringRefusal as At, red as B, parseBootstrapMarker as Bn, CdkdError as Br, s3BucketDomainName as Bt, refusesFinalSnapshot as C, exportNamesCarriedFrom as Cn, findLargeInlineResources as Cr, refStateLookupFromResource as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, AssetPublisher as Dn, expectedOwnerParam as Dr, assertRegionMatch as Dt, extractDeploymentEventError as E, shouldRetainResource as En, displaySafe as Er, resolveExplicitPhysicalId as Et, formatResourceLine as F, BOOTSTRAP_MARKER_PREFIX as Fn, AwsClients as Fr, requireConfigString as Ft, isExportAliasCollision as G, describeAwsFailure as Gn, DynamicReferenceRegionAmbiguousError as Gr, DiffCalculator as Gt, collectDeclaredOutputNames as H, validateAssetBucketName as Hn, CrossAccountSecretRefusalError as Hr, s3BucketRegionalDomainName as Ht, bold as I, assertAssetBucketRegion as In, getAwsClients as Ir, classifyReplaySecretRegion as It, secretSafeKeyDisplay as J, describeDockerExecFailure as Jn, LocalStartServiceError as Jr, findSilentDropProperties as Jt, secretBearing as K, buildDockerImage as Kn, IntrinsicResolutionRefusalError as Kr, INTRINSIC_KEYS as Kt, cyan as L, ensureAssetStorage as Ln, resetAwsClients as Lr, producerRegionsFromState as Lt, coerceWarmThroughput as M, loadPublishableAssetManifest as Mn, processStackMessages as Mr, replayWarn as Mt, isWarmThroughputDecrease as N, rewriteTemplateAssetReferences as Nn, clearBucketRegionCache as Nr, requireConfigArray as Nt, isStatefulRecreateTargetForReplace as O, stringifyValue as On, PARTITION_TABLE as Or, coerceCfnBoolean as Ot, toFiniteNumber as P, AssetModeResolver as Pn, resolveBucketRegion as Pr, requireConfigObject as Pt, clearOnUpdateRemoval as Q, getDockerCmd as Qn, ProvisioningError as Qr, TemplateParser as Qt, gray as R, getBootstrapMarkerKey as Rn, setAwsClients as Rr, withSharedDrainBudget as Rt, isFinalSnapshotError as S, DEFAULT_STATE_PREFIX as Sn, MIGRATE_TMP_PREFIX as Sr, parameterTypeMayLoseSecretIdentity as St, makeCanonicalizePropertiesFn as T, importableOutputs as Tn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as Tr, normalizeAwsTagsToCfn as Tt, collectPublishedOutputNames as U, validateContainerRepoName as Un, DependencyError as Ur, s3BucketWebsiteUrl as Ut, yellow as V, readBootstrapMarkerBody as Vn, ConfigError as Vr, s3BucketDualStackDomainName as Vt, exportAliasCollisionScrubWarning as W, buildDenyExternalAccessPolicy as Wn, DeployCancelledError as Wr, applyRoleArnIfSet as Wt, IAMRoleProvider as X, dockerSpawnEnvWithSensitive as Xn, NestedStackChildDirectDestroyError as Xr, withRetry as Xt, getCurrentResourceSecrets as Y, describeDockerFailure as Yn, LockError as Yr, describeTypeWithThrottleRetry as Yt, collectInlinePolicyNamesManagedBySiblings as Z, formatDockerLoginError as Zn, PartialFailureError as Zr, DagBuilder as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, buildForceUnlockCommand as _n, resolveUseCdkBootstrapAssets as _r, carriesDynamicReference as _t, DeploymentEventsStore as a, formatError as ai, errorCauseChain as an, AssetManifestLoader as ar, endCommandInterruptScope as at, ccRoutedFinalSnapshotError as b, shellQuote as bn, CFN_TEMPLATE_BODY_LIMIT as br, getAccountInfo as bt, replayFailedOperations as c, withErrorHandling as ci, maskSecretsInText as cn, synthesisStatusMessage as cr, startInterruptWatch as ct, updatePartialReason as d, isThrottlingError as di, redactSecretsForState as dn, resolveApp as dr, UNSPECIFIED_SKIP_REASON as dt, ResourceUpdateNotSupportedError as ei, STATE_SOURCED_READBACK_RULES as en, redactDockerArgvValues as er, wouldReturnToSdkProvider as et, withResourceDeadline as f, isTransientServerError as fi, scrubResourceRecord as fn, resolveAutoAssetStorage as fr, deleteIndeterminateGuards as ft, bindingSkippedOutputs as g, __exportAll as gi, UNRENDERABLE as gn, resolveStateBucketWithDefaultAndSource as gr, IntrinsicFunctionResolver as gt, computeImplicitDeleteEdges as h, retryClassificationText as hi, rebuildClientForBucketRegion as hn, resolveStateBucketWithDefault as hr, isTerminationProtectionPropagationError as ht, DeploymentEventsReader as i, SynthesisError as ii, dynamicReferenceTokens as in, stripControlChars as ir, beginCommandInterruptScope as it, WARM_THROUGHPUT_MEMBERS as j, createAssetRedirectResolver as jn, AssemblyReader as jr, readConfigString as jt, isStatefulRecreateTargetSync as k, WorkGraph as kn, canonicalizeRegion as kr, configBooleanRefusal as kt, replayRollback as l, isMarkedNonRetryable as li, recordMaskOnlyValue as ln, getDefaultStateBucketName as lr, CloudControlProvider as lt, IMPLICIT_DELETE_DEPENDENCIES as m, markRedactedCause as mi, S3StateBackend as mn, resolveSkipPrefix as mr, disableInstanceApiTermination as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, StackTerminationProtectionError as ni, carriesSecretMask as nn, runDockerStreaming as nr, maskDeep as nt, planFailedOps as o, isCdkdError as oi, isSingleDynamicReferenceToken as on, getDockerImageBySourceHash as or, interruptWatchListenerCount as ot, maskingRetryLogger as p, markNonRetryable as pi, LockManager as pn, resolveCaptureObservedState as pr, deleteSkipReason as pt, secretBearingStateKeyWarning as q, describeDockerCapturedOutput as qn, LocalInvokeBuildError as qr, findActionableSilentDrops as qt, DeployEngine as r, StateError as ri, createSecretMasker as rn, escapeRegExp$1 as rr, maskerOrIdentity as rt, planRollback as s, normalizeAwsError as si, maskSecretsInError as sn, Synthesizer as sr, isInterruptedWaitError as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, StackHasActiveImportsError as ti, TEMPLATE_SOURCED_RULES as tn, runDockerForeground as tr, createMaskedRetryLogger as tt, updatePartialMessage as u, isRetryableTransientError as ui, recoverMaskedOutput as un, getLegacyStateBucketName as ur, slowCcOperationTimeoutMs as ut, PRE_DELETE_SNAPSHOT_TYPES as v, buildLockContentionMessage as vn, stateBucketExistenceConfirmed as vr, cfnRefValueFromPhysicalId as vt, unsupportedFinalSnapshotError as w, importableOutputKeys as wn, uploadCfnTemplate as wr, WAFv2WebACLProvider as wt, createPreDeleteFinalSnapshot as x, CUSTOM_RESOURCE_RESPONSE_PREFIX as xn, CFN_TEMPLATE_URL_LIMIT as xr, isUnboundTemplateParameter as xt, buildFinalSnapshotIdentifier as y, forceQuitRecoveryClause as yn, warnDeprecatedNoPrefixCliFlag as yr, coerceParameterTypedValue as yt, green as z, isCrossRegionRedirect as zn, AssetError as zr, s3BucketArn as zt };
|
|
38426
|
+
//# sourceMappingURL=deploy-engine-BiPxTgKT.js.map
|