@go-to-k/cdkd 0.288.2 → 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.
@@ -1,8 +1,8 @@
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-BRByznpy.js";
3
+ import { t as getCdkdVersion } from "./version-BmM4QJ9t.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
- import { randomUUID } from "node:crypto";
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";
7
7
  import { CloudControlClient, CreateResourceCommand, DeleteResourceCommand, GetResourceCommand, GetResourceRequestStatusCommand, ListResourcesCommand, UpdateResourceCommand } from "@aws-sdk/client-cloudcontrol";
8
8
  import { AttachRolePolicyCommand, CreateRoleCommand, DeleteRoleCommand, DeleteRolePermissionsBoundaryCommand, DeleteRolePolicyCommand, DetachRolePolicyCommand, GetRoleCommand, GetRolePolicyCommand, IAMClient, ListAttachedRolePoliciesCommand, ListInstanceProfilesForRoleCommand, ListRolePoliciesCommand, ListRoleTagsCommand, NoSuchEntityException, PutRolePermissionsBoundaryCommand, PutRolePolicyCommand, RemoveRoleFromInstanceProfileCommand, TagRoleCommand, UntagRoleCommand, UpdateAssumeRolePolicyCommand, UpdateRoleCommand } from "@aws-sdk/client-iam";
@@ -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 "";
@@ -9101,6 +9113,18 @@ function importableOutputs(state) {
9101
9113
  function exportNamesCarriedFrom(previous) {
9102
9114
  return previous.exportNames === void 0 ? {} : { exportNames: previous.exportNames };
9103
9115
  }
9116
+ /**
9117
+ * The `skippedOutputs` field to write when a save carries the PREVIOUS
9118
+ * record's `outputs` bag forward instead of re-resolving it (issue #2740). The
9119
+ * record describes that bag — which keys the deploy that wrote it could not
9120
+ * resolve — so it travels with the bag, exactly like {@link
9121
+ * exportNamesCarriedFrom}: absent stays absent, present stays as it was. A
9122
+ * writer that re-resolves outputs does NOT call this; it writes the set the
9123
+ * resolution produced, or omits the field when nothing was skipped.
9124
+ */
9125
+ function skippedOutputsCarriedFrom(previous) {
9126
+ return previous.skippedOutputs === void 0 ? {} : { skippedOutputs: previous.skippedOutputs };
9127
+ }
9104
9128
 
9105
9129
  //#endregion
9106
9130
  //#region src/types/rollback-journal.ts
@@ -16687,6 +16711,7 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
16687
16711
  "RoleArn"
16688
16712
  ]),
16689
16713
  silentDrop: /* @__PURE__ */ new Map([
16714
+ ["CapacityProviderConfiguration", "not yet implemented by cdkd"],
16690
16715
  ["FilesystemConfigurations", "not yet implemented by cdkd"],
16691
16716
  ["RequestHeaderConfiguration", "not yet implemented by cdkd"],
16692
16717
  ["Tags", "not yet implemented by cdkd"]
@@ -16805,7 +16830,8 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
16805
16830
  silentDrop: /* @__PURE__ */ new Map([
16806
16831
  ["EvaluationCriteria", "Absent from both the SDK PutMetricAlarm input and the aws-cdk-lib CfnAlarm L1 (a newer CFn-schema-only property ahead of SDK/CDK support); no wire path to forward it and no CDK app can emit it."],
16807
16832
  ["EvaluationInterval", "Absent from both the SDK PutMetricAlarm input and the aws-cdk-lib CfnAlarm L1 (a newer CFn-schema-only property ahead of SDK/CDK support); no wire path to forward it and no CDK app can emit it."],
16808
- ["EvaluationWindow", "not yet implemented by cdkd"]
16833
+ ["EvaluationWindow", "not yet implemented by cdkd"],
16834
+ ["WarmUpConfiguration", "not yet implemented by cdkd"]
16809
16835
  ]),
16810
16836
  createOnlyDrops: /* @__PURE__ */ new Set()
16811
16837
  }],
@@ -17633,7 +17659,7 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
17633
17659
  }],
17634
17660
  ["AWS::Glue::Connection", {
17635
17661
  handled: /* @__PURE__ */ new Set(["CatalogId", "ConnectionInput"]),
17636
- silentDrop: /* @__PURE__ */ new Map(),
17662
+ silentDrop: /* @__PURE__ */ new Map([["Tags", "not yet implemented by cdkd"]]),
17637
17663
  createOnlyDrops: /* @__PURE__ */ new Set()
17638
17664
  }],
17639
17665
  ["AWS::Glue::Crawler", {
@@ -18197,7 +18223,6 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
18197
18223
  ["UseLatestRestorableTime", "not yet implemented by cdkd"]
18198
18224
  ]),
18199
18225
  createOnlyDrops: /* @__PURE__ */ new Set([
18200
- "AvailabilityZones",
18201
18226
  "ClusterScalabilityType",
18202
18227
  "DBSystemId",
18203
18228
  "EngineMode",
@@ -19897,6 +19922,113 @@ function s3BucketWebsiteUrl(bucketName, region) {
19897
19922
  return `http://${bucketName}.s3-website${S3_WEBSITE_ENDPOINT_LEGACY_DASH_REGIONS.has(folded) ? "-" : "."}${folded}.${urlSuffix}`;
19898
19923
  }
19899
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
+
19900
20032
  //#endregion
19901
20033
  //#region src/deployment/secret-region-classification.ts
19902
20034
  /**
@@ -22120,6 +22252,170 @@ const MAX_LISTED_AVAILABLE_OUTPUTS = 10;
22120
22252
  */
22121
22253
  const dynamicReferenceRetryDelays = {};
22122
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
+ /**
22123
22419
  * Is `region` safe to build an AWS SDK client from?
22124
22420
  *
22125
22421
  * This is a SECURITY gate, not an AWS region registry, and the distinction
@@ -23022,7 +23318,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
23022
23318
  * Resolve all intrinsic functions in a value
23023
23319
  */
23024
23320
  async resolve(value, context) {
23025
- return await this.resolveValue(value, context);
23321
+ return await withSharedDrainBudget(() => this.resolveValue(value, context));
23026
23322
  }
23027
23323
  /**
23028
23324
  * Evaluate all conditions in the template
@@ -23064,7 +23360,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
23064
23360
  }
23065
23361
  };
23066
23362
  for (const name of Object.keys(templateConditions)) try {
23067
- await evaluateByName(name);
23363
+ await withSharedDrainBudget(() => evaluateByName(name));
23068
23364
  } catch (error) {
23069
23365
  this.logger.warn(this.maskSecretsForLog(`Failed to evaluate condition ${name}: ${error instanceof Error ? error.message : String(error)}, assuming false`, maskingContext));
23070
23366
  conditions[name] = false;
@@ -23080,7 +23376,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
23080
23376
  if (typeof value === "string" && value.includes("{{resolve:")) return await this.resolveDynamicReferences(value, context);
23081
23377
  return value;
23082
23378
  }
23083
- if (Array.isArray(value)) return (await Promise.all(value.map((v) => this.resolveValue(v, context)))).filter((v) => v !== AWS_NO_VALUE);
23379
+ if (Array.isArray(value)) return (await allSettledKeepingFirstRejection(value.map((v) => this.resolveValue(v, context)))).filter((v) => v !== AWS_NO_VALUE);
23084
23380
  const obj = value;
23085
23381
  if ("Ref" in obj) return await this.resolveRef(obj["Ref"], context);
23086
23382
  if ("Fn::GetAtt" in obj) return await this.resolveGetAtt(obj["Fn::GetAtt"], context);
@@ -23693,6 +23989,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
23693
23989
  case "ApiId": return physicalId;
23694
23990
  default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
23695
23991
  }
23992
+ if (resourceType === "AWS::ApiGatewayV2::Api") switch (attributeName) {
23993
+ case "ExecuteApiArn": return `arn:${partition}:execute-api:${region}:${accountId}:${physicalId}`;
23994
+ case "ApiId": return physicalId;
23995
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
23996
+ }
23696
23997
  if (resourceType === "AWS::ServiceDiscovery::PrivateDnsNamespace" || resourceType === "AWS::ServiceDiscovery::HttpNamespace" || resourceType === "AWS::ServiceDiscovery::PublicDnsNamespace") switch (attributeName) {
23697
23998
  case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:namespace/${physicalId}`;
23698
23999
  case "Id": return physicalId;
@@ -23902,7 +24203,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
23902
24203
  let values = rawValues;
23903
24204
  if (!Array.isArray(values)) values = await this.resolveValue(values, context);
23904
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}`);
23905
- let result = (await Promise.all(values.map(async (v) => {
24206
+ let result = (await allSettledKeepingFirstRejection(values.map(async (v) => {
23906
24207
  const resolved = await this.resolveValue(v, context);
23907
24208
  return String(resolved);
23908
24209
  }))).join(delimiter);
@@ -26642,7 +26943,7 @@ var CloudControlProvider = class {
26642
26943
  const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
26643
26944
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
26644
26945
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
26645
- const { ASGProvider } = await import("./asg-provider-DUlRn84u.js").then((n) => n.n);
26946
+ const { ASGProvider } = await import("./asg-provider-TexsJufw.js").then((n) => n.n);
26646
26947
  const asgProvider = new ASGProvider();
26647
26948
  return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
26648
26949
  }
@@ -31119,6 +31420,134 @@ function isExportAliasCollision(exportName, outputKey, ownedOutputNames) {
31119
31420
  */
31120
31421
  const MIN_SECRET_NEEDLE = 4;
31121
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
+ /**
31122
31551
  * Which recorded secrets are visible in `text`, or `undefined` when none is.
31123
31552
  *
31124
31553
  * ONE rule for both callers below, because they were inconsistent and the
@@ -31143,8 +31572,14 @@ const MIN_SECRET_NEEDLE = 4;
31143
31572
  */
31144
31573
  function secretsPresentIn(text, secrets) {
31145
31574
  if (!secrets || secrets.size === 0) return void 0;
31575
+ const haystack = canonicalForSecretScan(text);
31146
31576
  const exposure = /* @__PURE__ */ new Map();
31147
- for (const [plaintext, expression] of secrets) if (text === plaintext || plaintext.length >= MIN_SECRET_NEEDLE && text.includes(plaintext)) exposure.set(plaintext, expression);
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
+ }
31148
31583
  return exposure.size > 0 ? exposure : void 0;
31149
31584
  }
31150
31585
  /**
@@ -31234,10 +31669,76 @@ function maskEveryOccurrence(text, exposure) {
31234
31669
  * unchanged. stderr is a reader like any other, so the invariant is absolute: a
31235
31670
  * message must never claim a masking it did not perform.
31236
31671
  */
31237
- function secretBearingExportNameWarning(outputKey, exportName, exposure) {
31238
- const masked = stripControlChars(maskEveryOccurrence(exportName, exposure));
31239
- const shown = masked === stripControlChars(exportName) ? "" : `(masked: "${masked}") `;
31240
- return `Output ${stripControlChars(outputKey)} has an Export.Name that resolves to a value containing a secret ${shown}— skipping the export alias. An export name becomes a key in state.json and in the exports index, and redaction rewrites VALUES only, so publishing it would persist the secret in plaintext. Use a non-secret Export.Name.`;
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.`;
31679
+ }
31680
+ /**
31681
+ * Test `key` for recorded secret plaintext and return how it may be shown.
31682
+ *
31683
+ * Reuses {@link stateKeySecretExposure} and the same `maskEveryOccurrence` the
31684
+ * warnings below use, rather than restating either: two spellings of "is this
31685
+ * key safe to print" would disagree on the boundary cases those two encode
31686
+ * (the whole-key match for a sub-floor needle, longest-needle-first masking).
31687
+ *
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 {
31708
+ kind: "safe",
31709
+ text: shown
31710
+ };
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" };
31717
+ return {
31718
+ kind: "masked",
31719
+ text: masked
31720
+ };
31721
+ }
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;
31241
31742
  }
31242
31743
  /**
31243
31744
  * Warning for a state KEY that already holds secret plaintext — the residue an
@@ -31250,8 +31751,9 @@ function secretBearingExportNameWarning(outputKey, exportName, exposure) {
31250
31751
  * redeploy, which rewrites `state.outputs` wholesale and republishes the index.
31251
31752
  * Reported so the `--dry-run --fail` CI gate stops calling such a state clean.
31252
31753
  */
31253
- function secretBearingStateKeyWarning(stackName, key, exposure) {
31254
- return `State for ${stripControlChars(stackName)} holds an output KEY containing a secret (masked: "${stripControlChars(maskEveryOccurrence(key, exposure))}") cdkd scrub cannot rewrite a key, only a value, because the key IS the export name consumers resolve by. Give that output a non-secret Export.Name and redeploy: the next deploy replaces state.outputs and the exports index entirely. ROTATE the exposed secret.`;
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.`;
31255
31757
  }
31256
31758
  /**
31257
31759
  * Warning for an `Export.Name` colliding with an output NAME it does not own.
@@ -31260,13 +31762,18 @@ function secretBearingStateKeyWarning(stackName, key, exposure) {
31260
31762
  * says which value survives — the export is skipped, so the key keeps the
31261
31763
  * output's own value.
31262
31764
  *
31263
- * No masking here, and none is needed: the name printed is one that MATCHED a
31264
- * declared output name, i.e. template text, and a name carrying a secret is
31265
- * refused by {@link secretBearingExportNameWarning} before this is reached.
31266
- */
31267
- function exportAliasCollisionWarning(outputKey, exportName) {
31268
- const shown = stripControlChars(exportName);
31269
- const from = stripControlChars(outputKey);
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));
31270
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.`;
31271
31778
  }
31272
31779
  /**
@@ -31281,12 +31788,12 @@ function exportAliasCollisionWarning(outputKey, exportName) {
31281
31788
  * {@link collectDeclaredOutputNames}.
31282
31789
  */
31283
31790
  function exportAliasCollisionScrubWarning(outputKey, exportName, secrets) {
31284
- const mask = (name) => {
31285
- const exposure = secretsPresentIn(name, secrets);
31286
- return stripControlChars(exposure ? maskEveryOccurrence(name, exposure) : name);
31287
- };
31288
- const shown = mask(exportName);
31289
- return `Output ${mask(outputKey)} exports as "${shown}", which is also the name of another output in this stack — state cannot say which of the two the stored value under "${shown}" came from, so that key is redacted by value match instead of by template position, and two references resolving to the same value could still collapse there. Rename the export, or the colliding output, and redeploy.`;
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.`;
31290
31797
  }
31291
31798
 
31292
31799
  //#endregion
@@ -32682,6 +33189,397 @@ async function createReplicationGroupFinalSnapshot(client, replicationGroupId, l
32682
33189
  });
32683
33190
  }
32684
33191
 
33192
+ //#endregion
33193
+ //#region src/analyzer/skipped-outputs.ts
33194
+ /**
33195
+ * The record `cdkd deploy` leaves behind for an Output it could NOT resolve
33196
+ * and SKIPPED, and the digest `cdkd diff` compares it against (issue
33197
+ * [#2740](https://github.com/go-to-k/cdkd/issues/2740)).
33198
+ *
33199
+ * The deploy side (`DeployEngine.handleOutputResolutionFailure`, default arm)
33200
+ * warns, stores `undefined` for the key and moves on, so a re-resolved
33201
+ * `state.outputs` lacks it (the no-change path keeps the previous bag whole
33202
+ * when any output fails, so a key that resolved on an EARLIER deploy can keep
33203
+ * its value beside a record — the reader checks absence first and ignores the
33204
+ * record then). The diff side resolves outputs with `skipDynamicReferences`, so a
33205
+ * failure that happens INSIDE a secret lookup — a JSON key the secret does not
33206
+ * hold, a reference assembled from another secret's value — does not reproduce
33207
+ * there: the value assembles into its token, the key is absent from state, and
33208
+ * `computeOutputsDiff` pushed an `ADD` the deploy would never perform, on every
33209
+ * run of an unchanged stack. The diff cannot tell from assembly alone that the
33210
+ * deploy would fail (a composed string carrying a secret reference is a
33211
+ * legitimate output), so it learns what the deploy learned instead:
33212
+ * `StackState.skippedOutputs` maps each skipped key to a digest of the
33213
+ * template inputs its resolution read, and the diff trusts the record only
33214
+ * while that digest is unchanged.
33215
+ *
33216
+ * ## What the digest covers, and why
33217
+ *
33218
+ * `skippedOutputDigest` hashes the output's OWN entry (`Value`, `Export`,
33219
+ * `Condition`, whatever else it declares) together with every top-level
33220
+ * template section EXCEPT `Resources` and `Outputs`:
33221
+ *
33222
+ * - The entry itself: repairing the `Value` expression is the obvious repair.
33223
+ * The `Export.Name` is in it too, because the alias pass routes an
33224
+ * `Export.Name` failure through the same handler, which blanks the output's
33225
+ * own key even when its value resolved — so a repaired or removed export
33226
+ * name must invalidate the record as well.
33227
+ * - `Parameters` (a `Ref`-built reference repaired through a parameter
33228
+ * default), `Conditions` (an `Fn::If` whose branch flips), `Mappings` (an
33229
+ * `Fn::FindInMap`-built key), and the remaining sections for the same
33230
+ * reason, spelled as one rule rather than a list that drifts: everything an
33231
+ * output's resolution can read besides resources. One VALUE inside them is
33232
+ * excluded — the `Default` of a `NoEcho: true` parameter, hashed as a
33233
+ * constant; see `withNoEchoDefaultsMasked` for why. That is the POSITION,
33234
+ * not the parameter: its `AllowedValues`, and any literal an author puts in
33235
+ * `Conditions`, `Rules` or `Metadata`, are hashed as before.
33236
+ * - NOT `Resources`: hashing the section would invalidate the record on every
33237
+ * unrelated resource edit. The output CAN be repaired from that side —
33238
+ * `collectSkippedOutputs` records both skip arms, and the quiet one is an
33239
+ * `Fn::GetAtt` whose attribute `constructAttribute` could not build — so a
33240
+ * digest alone would keep binding while the next deploy publishes the row
33241
+ * and its `Export.Name`, the phantom's inverse. {@link
33242
+ * bindingSkippedOutputs} closes that with the CHANGE MAP rather than the
33243
+ * digest: a key whose entry references a logical id with a pending resource
33244
+ * change does not bind. The diff has that map (it ran the resource diff
33245
+ * first); the deploy does not need it, because a stack with any resource
33246
+ * change takes the path that re-resolves every output and rewrites the
33247
+ * record. REFERENCE, not repair: whether an edit could actually make the
33248
+ * output resolvable is undecidable from a template, so an unrelated edit to
33249
+ * a referenced resource un-binds too and the key can preview as an `ADD`
33250
+ * again. The cost is bounded — a run with a changed resource is already
33251
+ * reporting that row, so `--fail` exits 1 regardless — while the case it
33252
+ * buys depends on whether the diff can resolve the output at all. For an
33253
+ * output reading an attribute that does not exist yet — the SECOND skip arm
33254
+ * this record covers, not the secret lookup issue #2740 was filed on — it is
33255
+ * the VERDICT and not the row: unresolvable in
33256
+ * both readings, so bound the diff reports the outputs settled while the
33257
+ * next deploy is about to publish that key and its `Export.Name`. For an
33258
+ * output the diff CAN resolve, whose reference is to a name rather than to
33259
+ * a pending attribute (an `Fn::Sub` over an SSM parameter's name, say), the
33260
+ * row IS the difference and reappears as an `ADD` the moment the record
33261
+ * stops binding.
33262
+ * - NOT the sibling `Outputs`: a CloudFormation output cannot reference
33263
+ * another output, so a sibling change cannot repair this one, and excluding
33264
+ * them keeps an unrelated output edit from producing a one-run phantom.
33265
+ *
33266
+ * What the digest CANNOT see is a repair outside the template — the secret
33267
+ * gained the JSON key, the SSM parameter was created, a parameter VALUE
33268
+ * handed to the engine rather than declared changed (a nested stack's inputs
33269
+ * from its parent, `DeployEngineOptions.parameters`; `Parameters`
33270
+ * DECLARATIONS are hashed, supplied values are not), or **cdkd itself was
33271
+ * upgraded**: the quiet skip arm is `constructAttribute` returning nothing,
33272
+ * so a provider that gains that attribute repairs the output while neither
33273
+ * the template nor any resource moves.
33274
+ *
33275
+ * They are blind spots for three different reasons, and only the first is
33276
+ * about knowledge. The secret's key set and the SSM parameter's existence
33277
+ * need the external lookup the diff deliberately refuses to make. A supplied
33278
+ * parameter VALUE is not one of those — a nested stack's inputs are resolved
33279
+ * and forwarded to the child diff (`resolveChildStackParameters`) — it is
33280
+ * simply outside what the digest hashes, because hashing values would make
33281
+ * the record depend on a caller's arguments rather than on the template. And
33282
+ * the writing binary's version is knowable and merely unstored: keeping it
33283
+ * beside the digest and un-binding on mismatch would close that one. All four
33284
+ * end the same way: the record keeps binding until the next deploy re-resolves
33285
+ * the output, and that deploy publishes the key — unless a SIBLING output is
33286
+ * still unresolved on a run with no resource change, where the engine keeps
33287
+ * the previous outputs bag wholesale and the key stays unpublished for a
33288
+ * further run (go-to-k/cdkd#2771, pre-existing and not introduced here).
33289
+ * Documented as the accepted limitation in `docs/cli-diff.md`; it is narrower
33290
+ * than the pre-#2740 behaviour, where the diff was wrong on EVERY run.
33291
+ *
33292
+ * A repair on the RESOURCE side is NOT on that list either, but the reason is
33293
+ * narrower than it first looks. The change map above closes it only when the
33294
+ * repair arrives WITH a template resource change, which is what makes the
33295
+ * resource show on the diff at all. An OUT-OF-BAND state write does not:
33296
+ * every writer that rebuilds state outside a deploy DROPS the record rather
33297
+ * than carry it (one enumerated exception, the partial-destroy snapshot, is
33298
+ * explained with the field) — `cdkd import` refreshes `attributes` for the
33299
+ * resources it imports, `cdkd drift --accept` rewrites the properties an
33300
+ * attribute may be built from, `cdkd rollback`'s replacement arm rebuilds a
33301
+ * record with the attributes a FRESH create returned, and the rest are listed
33302
+ * with the field.
33303
+ * Dropping returns those keys to pre-#2740 behaviour until the next deploy
33304
+ * recomputes the record — a row where the diff can resolve them, the ordinary
33305
+ * whole-section suppression where it cannot — which is what keeps a
33306
+ * resource-side repair out of the list above.
33307
+ *
33308
+ * The rule is flat and it is meant to be: EVERY writer that rebuilds state
33309
+ * outside a deploy drops the record, the partial-destroy snapshot being the
33310
+ * one enumerated exception — the full list lives with the field, in
33311
+ * `StackState.skippedOutputs`. Three per-writer arguments for carrying it were
33312
+ * written during review, which is the reason for the flat rule rather than an
33313
+ * aside: two were shown wrong (substituting a value repairs an output whose
33314
+ * enclosing intrinsic was choking on what was there; scrubbing a plaintext
33315
+ * back to its expression rewrites a string outputs read verbatim) and the
33316
+ * third — deleting a property EXPOSING an attribute of the same name — could
33317
+ * not be settled either way, which argues for flatness at least as strongly.
33318
+ *
33319
+ * ## Where the digest must be taken from
33320
+ *
33321
+ * The template AS HANDED to the engine / to `computeStackDiff` — before
33322
+ * parameter binding, condition evaluation or any output resolution. The
33323
+ * INVARIANT, and the reason both callers `structuredClone` rather than pass
33324
+ * their live object: **no resolution may be visible to the digest, on either
33325
+ * side, and both sides must take it at the same point of their own flow.**
33326
+ * Two independent reasons, neither of which depends on any particular
33327
+ * resolver doing the writing: a digest that could see a resolved value would
33328
+ * (1) differ between the two sides, which computes a record the diff can
33329
+ * never re-derive, and (2) fingerprint a secret into `state.json`, since a
33330
+ * resolved value can be a decrypted one.
33331
+ *
33332
+ * The clone is therefore NOT dead weight even while no resolver mutates its
33333
+ * input. It used to be load-bearing against a live rewrite — `resolveSub`
33334
+ * substituted into the caller's two-argument `Fn::Sub` variable map until
33335
+ * go-to-k/cdkd#2764 gave it a fresh object — and the protection was never
33336
+ * derived from that call site: it is derived from the invariant above, which
33337
+ * a future in-place optimisation anywhere in either flow would silently
33338
+ * violate. `tests/unit/cli/diff-recursive-skipped-outputs.test.ts` pins the
33339
+ * current no-mutation contract from the other direction (the template handed
33340
+ * to `computeStackDiff` comes back byte-identical), so reintroducing a
33341
+ * rewrite reds a test rather than moving a digest;
33342
+ * `tests/unit/deployment/deploy-engine-skipped-outputs.test.ts` INJECTS a
33343
+ * rewrite through its resolver mock and requires the recorded digest to equal
33344
+ * a fresh parse's, which pins the ordering itself.
33345
+ *
33346
+ * Given that contract the hash covers template text only. It is over
33347
+ * canonical JSON (object keys sorted at every level), so a template
33348
+ * re-serialised with keys in another order digests identically.
33349
+ *
33350
+ * ## What the diff does with a binding record
33351
+ *
33352
+ * It previews the key as ABSENT — no row — because that is exactly what the
33353
+ * deploy will leave in state. It does NOT flag the section as failed: a
33354
+ * sibling output that genuinely changed still renders, and `--fail` still
33355
+ * exits 1 for it. A record whose digest no longer matches is simply ignored:
33356
+ * the output is previewed under the ordinary rules again (usually an `ADD`;
33357
+ * an intrinsic `Export.Name` the diff still cannot resolve keeps omitting the
33358
+ * section, as before), the next deploy re-decides it —
33359
+ * publishing it if the repair took, or recording it again under the new
33360
+ * digest — and the diff follows that decision.
33361
+ */
33362
+ /**
33363
+ * JSON with object keys sorted at every level. Arrays keep their order (an
33364
+ * `Fn::Join` list is positional). `undefined` members are dropped, as
33365
+ * `JSON.stringify` drops them, so a parsed template and its in-memory twin
33366
+ * digest identically.
33367
+ */
33368
+ function canonicalJson(value) {
33369
+ return JSON.stringify(value, (_key, v) => {
33370
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) {
33371
+ const sorted = Object.create(null);
33372
+ for (const k of Object.keys(v).sort()) sorted[k] = v[k];
33373
+ return sorted;
33374
+ }
33375
+ return v;
33376
+ });
33377
+ }
33378
+ /** The constant a `NoEcho` `Default` is hashed as. Its text is irrelevant; that
33379
+ * it is CONSTANT is the point. */
33380
+ const NO_ECHO_DEFAULT_MASK = "<cdkd:noecho-default>";
33381
+ /**
33382
+ * `Parameters` with every `NoEcho: true` parameter's `Default` replaced by a
33383
+ * constant, for hashing only.
33384
+ *
33385
+ * The pre-resolution snapshot already keeps RESOLVED secret values out of the
33386
+ * digest. An authored `Default` under `NoEcho: true` is not one of those — it
33387
+ * is template text, and it went into the hash. With the rest of the
33388
+ * non-`Resources` sections known, a reader of `state.json` holds a confirm
33389
+ * ORACLE for a low-entropy default: guess it, recompute, compare.
33390
+ *
33391
+ * This does NOT claim the value is otherwise absent from state — a parameter a
33392
+ * resource reads can persist its RESOLVED default in that resource's
33393
+ * properties, and parameter `NoEcho` is outside the state-redaction model
33394
+ * either way. What it claims is narrower and still worth having: this record
33395
+ * must not ADD an oracle of its own for the case where the value is not
33396
+ * already there.
33397
+ *
33398
+ * The cost, stated rather than hidden: changing ONLY such a default no longer
33399
+ * moves the digest, so the record keeps binding through that edit until the
33400
+ * next deploy re-resolves the output. That is the same bounded staleness the
33401
+ * documented blind spots carry.
33402
+ *
33403
+ * Everything else about the parameter is still hashed — its `Type`, its
33404
+ * `AllowedValues`, the `NoEcho` flag itself — so a parameter appearing,
33405
+ * disappearing or changing shape still un-binds. Unmatched parameter content
33406
+ * hashes exactly as before: the map is rebuilt, but the canonicaliser sorts
33407
+ * keys and ignores prototypes, so a template with no `NoEcho` default gets the
33408
+ * digest it always had.
33409
+ */
33410
+ function withNoEchoDefaultsMasked(parameters) {
33411
+ if (parameters === null || typeof parameters !== "object" || Array.isArray(parameters)) return parameters;
33412
+ const masked = Object.create(null);
33413
+ for (const [name, decl] of Object.entries(parameters)) {
33414
+ const body = decl;
33415
+ masked[name] = typeof body === "object" && body !== null && body["NoEcho"] === true && "Default" in body ? {
33416
+ ...body,
33417
+ Default: NO_ECHO_DEFAULT_MASK
33418
+ } : decl;
33419
+ }
33420
+ return masked;
33421
+ }
33422
+ /**
33423
+ * The digest recorded in `StackState.skippedOutputs[outputKey]` by the deploy
33424
+ * and recomputed by the diff — see the module doc for what it covers. Both
33425
+ * sides call THIS function, so the coverage is spelled once.
33426
+ *
33427
+ * `template` must be the pre-resolution snapshot the module doc describes.
33428
+ */
33429
+ function skippedOutputDigest(template, outputKey) {
33430
+ const sections = Object.create(null);
33431
+ for (const [section, body] of Object.entries(template)) {
33432
+ if (section === "Resources" || section === "Outputs") continue;
33433
+ sections[section] = section === "Parameters" ? withNoEchoDefaultsMasked(body) : body;
33434
+ }
33435
+ const entry = template.Outputs?.[outputKey];
33436
+ return createHash("sha256").update(canonicalJson({
33437
+ output: entry,
33438
+ template: sections
33439
+ })).digest("hex");
33440
+ }
33441
+ /**
33442
+ * The record a deploy writes for the bag `resolveOutputs` produced: every key
33443
+ * whose value is `undefined`, mapped to its digest. Two arms write that value:
33444
+ * the default arm of `handleOutputResolutionFailure` (the resolver THREW —
33445
+ * warned, and refused under `--strict-getatt`), and a resolver that returned
33446
+ * `undefined` outright without throwing (an attribute it could construct
33447
+ * nothing for, `constructAttribute`'s empty arm — no warn, no strict refusal).
33448
+ * Both leave the key out of a re-resolved bag (the no-change path may keep a
33449
+ * previous value under it, see the module doc), and that path's
33450
+ * `resolutionFailed` treats both the same, so this record does too: the
33451
+ * question it answers is "will the next deploy leave this key absent?", and
33452
+ * the answer is yes either way. `undefined` (the field OMITTED) when nothing
33453
+ * was skipped, so a record that carries the field says at least one output
33454
+ * was skipped.
33455
+ *
33456
+ * `template` must be the pre-resolution snapshot (see the module doc).
33457
+ */
33458
+ function collectSkippedOutputs(template, resolvedOutputs) {
33459
+ let record;
33460
+ for (const [outputKey, value] of Object.entries(resolvedOutputs)) {
33461
+ if (value !== void 0) continue;
33462
+ record ??= Object.create(null);
33463
+ record[outputKey] = skippedOutputDigest(template, outputKey);
33464
+ }
33465
+ return record;
33466
+ }
33467
+ /**
33468
+ * Every resource logical id an output entry could reference, from `Ref`,
33469
+ * either `Fn::GetAtt` spelling, and an `Fn::Sub` placeholder, walked to any
33470
+ * depth over the whole entry (`Value`, `Export.Name` and anything else it
33471
+ * declares).
33472
+ *
33473
+ * A deliberate SUPERSET: an `Fn::Sub` placeholder naming a template
33474
+ * PARAMETER, or a `Ref` to one, lands here too, since telling the two apart
33475
+ * needs the `Parameters` section and the answer only ever widens the set. The
33476
+ * one consumer intersects it with the ids that actually have a pending
33477
+ * RESOURCE change, so a name that is not a resource cannot match, and a
33478
+ * parameter deliberately named after a changed resource merely un-binds one
33479
+ * record — the direction that previews a row rather than hiding one. Pseudo
33480
+ * parameters (`AWS::…`) and the `${!Literal}` escape are dropped.
33481
+ */
33482
+ function referencedLogicalIds(entry) {
33483
+ const ids = /* @__PURE__ */ new Set();
33484
+ const addName = (name) => {
33485
+ const logicalId = name.split(".")[0];
33486
+ if (logicalId !== void 0 && logicalId !== "" && !logicalId.startsWith("AWS::")) ids.add(logicalId);
33487
+ };
33488
+ const walk = (value) => {
33489
+ if (Array.isArray(value)) {
33490
+ for (const item of value) walk(item);
33491
+ return;
33492
+ }
33493
+ if (value === null || typeof value !== "object") return;
33494
+ const obj = value;
33495
+ if (typeof obj["Ref"] === "string") addName(obj["Ref"]);
33496
+ const getAtt = obj["Fn::GetAtt"];
33497
+ if (typeof getAtt === "string") addName(getAtt);
33498
+ else if (Array.isArray(getAtt) && typeof getAtt[0] === "string") addName(getAtt[0]);
33499
+ for (const [key, nested] of Object.entries(obj)) {
33500
+ if (key === "Fn::Sub") {
33501
+ walkSub(nested);
33502
+ continue;
33503
+ }
33504
+ walk(nested);
33505
+ }
33506
+ };
33507
+ /**
33508
+ * `Fn::Sub` in both spellings. Handled here rather than by the generic walk
33509
+ * because the two-argument form's second element is a VARIABLE MAP, not a
33510
+ * template fragment: its KEYS are variable names, so letting the generic
33511
+ * walk see `{ Ref: 'Unrelated' }` there would read a variable literally
33512
+ * named `Ref` as a `Ref` intrinsic. The map's VALUES are walked, since the
33513
+ * resolver resolves them.
33514
+ */
33515
+ const walkSub = (sub) => {
33516
+ const isPair = Array.isArray(sub);
33517
+ const subTemplate = typeof sub === "string" ? sub : isPair ? sub[0] : void 0;
33518
+ const variableMap = isPair && sub[1] !== null && typeof sub[1] === "object" ? sub[1] : void 0;
33519
+ if (typeof subTemplate === "string") {
33520
+ const declaredVars = new Set(variableMap === void 0 ? [] : Object.keys(variableMap));
33521
+ for (const [, placeholder] of subTemplate.matchAll(/\$\{([^}]*)\}/g)) {
33522
+ if (placeholder === void 0 || placeholder.startsWith("!")) continue;
33523
+ if (declaredVars.has(placeholder)) continue;
33524
+ addName(placeholder);
33525
+ }
33526
+ }
33527
+ if (isPair) for (const [index, element] of sub.entries()) {
33528
+ if (index === 1) {
33529
+ if (variableMap !== void 0) for (const declared of Object.values(variableMap)) walk(declared);
33530
+ continue;
33531
+ }
33532
+ walk(element);
33533
+ }
33534
+ else walk(sub);
33535
+ };
33536
+ walk(entry);
33537
+ return ids;
33538
+ }
33539
+ /**
33540
+ * The keys of a `StackState.skippedOutputs` record that still BIND against
33541
+ * today's template: recorded digest equal to the digest of the same key over
33542
+ * `pristineTemplate`, which must be the pre-resolution snapshot the module doc
33543
+ * describes. A key the template no longer declares is skipped outright — the
33544
+ * record is inert for it, and the next deploy clears it. The diff passes the
33545
+ * result to `resolveTemplateOutputs`, which adds the one check only state can
33546
+ * answer — that the key is still absent from the stored bag.
33547
+ *
33548
+ * `changedLogicalIds` is the second half of the binding rule, and the one the
33549
+ * digest structurally cannot carry (see the module doc's `Resources` bullet):
33550
+ * the logical ids this run's resource diff reports as changing. An output
33551
+ * whose entry references one of them does NOT bind, because the deploy that
33552
+ * follows takes the changed-resources path, re-resolves every output, and may
33553
+ * publish the very key the record would have hidden. Omitted by a caller with
33554
+ * no resource diff in hand — every such caller today is a test asserting the
33555
+ * digest half alone.
33556
+ */
33557
+ function bindingSkippedOutputs(pristineTemplate, record, changedLogicalIds) {
33558
+ const binding = /* @__PURE__ */ new Set();
33559
+ if (record === null || record === void 0) return binding;
33560
+ const declared = pristineTemplate.Outputs ?? {};
33561
+ for (const [outputKey, digest] of Object.entries(record)) {
33562
+ if (!Object.prototype.hasOwnProperty.call(declared, outputKey)) continue;
33563
+ const entry = declared[outputKey];
33564
+ if (digest !== skippedOutputDigest(pristineTemplate, outputKey)) continue;
33565
+ if (changedLogicalIds !== void 0 && [...referencedLogicalIds(entry)].some((id) => changedLogicalIds.has(id))) continue;
33566
+ binding.add(outputKey);
33567
+ }
33568
+ return binding;
33569
+ }
33570
+ /**
33571
+ * Whether two records — either possibly absent — describe the same skipped
33572
+ * set with the same digests. The no-change deploy path persists on a
33573
+ * difference here: it is the ONLY save trigger for this field on that path,
33574
+ * because a skipped output switches the outputs-changed trigger off.
33575
+ */
33576
+ function skippedOutputsEqual(a, b) {
33577
+ const aKeys = a === null || a === void 0 ? [] : Object.keys(a);
33578
+ const bKeys = b === null || b === void 0 ? [] : Object.keys(b);
33579
+ if (aKeys.length !== bKeys.length) return false;
33580
+ return aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && a?.[k] === b?.[k]);
33581
+ }
33582
+
32685
33583
  //#endregion
32686
33584
  //#region src/analyzer/implicit-delete-deps.ts
32687
33585
  /**
@@ -35198,8 +36096,17 @@ var DeployEngine = class {
35198
36096
  * secret collapse onto whichever expression was recorded last, exactly as two
35199
36097
  * resource properties did before #1904. Captured in `resolveOutputs` at the
35200
36098
  * same point `outputSecrets` is accumulated. Reset per `deploy()`.
36099
+ *
36100
+ * Null-prototype for the same reason as the outputs bag `resolveOutputs`
36101
+ * builds two lines away: both are keyed by TEMPLATE-CONTROLLED output names,
36102
+ * so an output called `__proto__` would replace this bag's prototype and a
36103
+ * later lookup of a name it never stored would inherit from it. Only the
36104
+ * RESET in `deploy()` is pinned — reverting this initialiser alone survives
36105
+ * the suite, measured, because `deploy()` replaces the bag before anything
36106
+ * writes to it, so the initialiser matches the reset rather than standing as
36107
+ * a second guard.
35201
36108
  */
35202
- outputsTemplateSource = {};
36109
+ outputsTemplateSource = Object.create(null);
35203
36110
  /**
35204
36111
  * The export aliases the last `resolveOutputs` pass WROTE into its bag
35205
36112
  * (issue #2193) — exactly the keys `outputs[exportName] = value` landed on,
@@ -35212,6 +36119,18 @@ var DeployEngine = class {
35212
36119
  */
35213
36120
  resolvedExportNames = [];
35214
36121
  /**
36122
+ * The outputs the last `resolveOutputs` pass could NOT resolve and SKIPPED
36123
+ * (the resolver threw under the default arm of
36124
+ * `handleOutputResolutionFailure`, or returned `undefined` outright — see
36125
+ * `collectSkippedOutputs`), each mapped to the
36126
+ * digest `cdkd diff` compares against (issue #2740, `StackState.
36127
+ * skippedOutputs`). `undefined` when nothing was skipped, so the saves that
36128
+ * persist the resolved bag spread it as-is and the field stays omitted. Same
36129
+ * lifetime rule as `resolvedExportNames`: reset at the top of every
36130
+ * `resolveOutputs`, meaningful only right after that call returns.
36131
+ */
36132
+ skippedOutputs;
36133
+ /**
35215
36134
  * Whether {@link outputsTemplateSource} may be used to POSITION the outputs
35216
36135
  * redaction. False once an outputs pass threw partway: the post-loop
35217
36136
  * name pass never ran, so the bag holds only the alias keys written before
@@ -35298,8 +36217,9 @@ var DeployEngine = class {
35298
36217
  this.perResourceTemplateProps = /* @__PURE__ */ new Map();
35299
36218
  this.attemptedResolvedProps = /* @__PURE__ */ new Map();
35300
36219
  this.outputSecrets = /* @__PURE__ */ new Map();
35301
- this.outputsTemplateSource = {};
36220
+ this.outputsTemplateSource = Object.create(null);
35302
36221
  this.outputsSourceUsable = true;
36222
+ this.skippedOutputs = void 0;
35303
36223
  this.retainedOldOnReplacement = /* @__PURE__ */ new Set();
35304
36224
  this.resolver.resetPhysicalIdFallbackCount();
35305
36225
  return withStackName(stackName, () => this.doDeploy(stackName, template));
@@ -35783,6 +36703,10 @@ var DeployEngine = class {
35783
36703
  } catch {}
35784
36704
  this.kickOffAutoRefreshObservedProperties(currentState.resources);
35785
36705
  this.logger.debug(`Template has ${Object.keys(template.Resources || {}).length} resources`);
36706
+ const outputsDigestSource = structuredClone({
36707
+ ...template,
36708
+ Resources: {}
36709
+ });
35786
36710
  const parameterValues = await this.resolver.resolveParameters(template, this.options.parameters, { ...this.options.inheritedSecrets && this.options.inheritedSecrets.size > 0 && { inheritedSecrets: this.options.inheritedSecrets } });
35787
36711
  this.logger.debug(`Resolved ${Object.keys(parameterValues).length} parameters: ${Object.keys(parameterValues).join(", ")}`);
35788
36712
  const context = this.buildResolverContext({
@@ -35826,7 +36750,7 @@ var DeployEngine = class {
35826
36750
  this.logger.info("No changes detected. Stack is up to date.");
35827
36751
  let persistedOutputs = currentState.outputs ?? {};
35828
36752
  if (!this.options.dryRun) {
35829
- const resolvedOutputs = this.redactOutputs(await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, parameterValues, conditions));
36753
+ const resolvedOutputs = this.redactOutputs(await withSharedDrainBudget(() => this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, outputsDigestSource, parameterValues, conditions)));
35830
36754
  const resolutionFailed = Object.values(resolvedOutputs).some((v) => v === void 0);
35831
36755
  const outputsChanged = !resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs);
35832
36756
  const currentEffectiveExports = new Set(importableOutputKeys(currentState));
@@ -35835,7 +36759,8 @@ var DeployEngine = class {
35835
36759
  if (resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs)) this.logger.warn("Outputs changed but one or more could not be resolved; keeping the previously persisted outputs. A downstream Fn::ImportValue may fail until the next deploy.");
35836
36760
  const observedRefresh = this.observedCaptureTasks.size > 0;
35837
36761
  if (observedRefresh) await this.drainObservedCaptures(currentState.resources);
35838
- if (observedRefresh || outputsChanged || exportSetChanged) try {
36762
+ const skippedOutputsChanged = !skippedOutputsEqual(currentState.skippedOutputs, this.skippedOutputs);
36763
+ if (observedRefresh || outputsChanged || exportSetChanged || skippedOutputsChanged) try {
35839
36764
  const refreshedState = {
35840
36765
  version: 9,
35841
36766
  region: this.stackRegion,
@@ -35843,6 +36768,7 @@ var DeployEngine = class {
35843
36768
  resources: currentState.resources,
35844
36769
  outputs: outputsChanged ? resolvedOutputs : persistedOutputs,
35845
36770
  ...resolutionFailed ? exportNamesCarriedFrom(currentState) : { exportNames: [...this.resolvedExportNames] },
36771
+ ...this.skippedOutputs && { skippedOutputs: { ...this.skippedOutputs } },
35846
36772
  ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
35847
36773
  lastModified: Date.now()
35848
36774
  };
@@ -35855,7 +36781,8 @@ var DeployEngine = class {
35855
36781
  if (outputsChanged) this.logger.info("Persisted Outputs-only change (no resource diff).");
35856
36782
  else this.logger.debug("Persisted export-set change (no outputs-value diff, no-change path, #2193)");
35857
36783
  if (this.exportIndexStore) await this.exportIndexStore.updateForStack(stackName, this.stackRegion, importableOutputs(refreshedState));
35858
- } else this.logger.debug("Persisted refreshed observedProperties (no-change path)");
36784
+ } else if (observedRefresh) this.logger.debug("Persisted refreshed observedProperties (no-change path)");
36785
+ else this.logger.debug("Persisted skipped-outputs record (no outputs-value diff, no-change path, #2740)");
35859
36786
  } catch (saveError) {
35860
36787
  this.logger.warn(`Failed to persist no-change state update: ${saveError instanceof Error ? saveError.message : String(saveError)} — drift baseline / outputs will be re-resolved on next deploy.`);
35861
36788
  }
@@ -35897,7 +36824,7 @@ var DeployEngine = class {
35897
36824
  current: 0,
35898
36825
  total: createChanges.length + updateChanges.length + deleteChanges.length
35899
36826
  };
35900
- const { state: newState, actualCounts } = await this.executeDeployment(effectiveTemplate, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress, migrationPending);
36827
+ const { state: newState, actualCounts } = await this.executeDeployment(effectiveTemplate, currentState, changes, dag, executionLevels, stackName, outputsDigestSource, parameterValues, conditions, currentEtag, progress, migrationPending);
35901
36828
  await this.drainObservedCaptures(newState.resources);
35902
36829
  const newEtag = await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(newState));
35903
36830
  this.logger.debug(`State saved (ETag: ${newEtag})`);
@@ -35939,7 +36866,7 @@ var DeployEngine = class {
35939
36866
  * - DELETE follows reverse dependency order (a node starts as soon as all
35940
36867
  * resources that depend ON it have finished deleting)
35941
36868
  */
35942
- async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress, migrationPending = false) {
36869
+ async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, outputsDigestSource, parameterValues, conditions, currentEtag, progress, migrationPending = false) {
35943
36870
  const concurrency = this.options.concurrency;
35944
36871
  const newResources = { ...currentState.resources };
35945
36872
  const actualCounts = {
@@ -35965,6 +36892,7 @@ var DeployEngine = class {
35965
36892
  resources: newResources,
35966
36893
  outputs: currentState.outputs,
35967
36894
  ...exportNamesCarriedFrom(currentState),
36895
+ ...skippedOutputsCarriedFrom(currentState),
35968
36896
  ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
35969
36897
  lastModified: Date.now()
35970
36898
  };
@@ -36097,6 +37025,7 @@ var DeployEngine = class {
36097
37025
  resources: newResources,
36098
37026
  outputs: currentState.outputs,
36099
37027
  ...exportNamesCarriedFrom(currentState),
37028
+ ...skippedOutputsCarriedFrom(currentState),
36100
37029
  ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
36101
37030
  lastModified: Date.now()
36102
37031
  };
@@ -36133,6 +37062,7 @@ var DeployEngine = class {
36133
37062
  resources: newResources,
36134
37063
  outputs: currentState.outputs,
36135
37064
  ...exportNamesCarriedFrom(currentState),
37065
+ ...skippedOutputsCarriedFrom(currentState),
36136
37066
  ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
36137
37067
  lastModified: Date.now()
36138
37068
  };
@@ -36150,6 +37080,7 @@ var DeployEngine = class {
36150
37080
  resources: newResources,
36151
37081
  outputs: currentState.outputs,
36152
37082
  ...exportNamesCarriedFrom(currentState),
37083
+ ...skippedOutputsCarriedFrom(currentState),
36153
37084
  ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
36154
37085
  lastModified: Date.now()
36155
37086
  };
@@ -36164,7 +37095,7 @@ var DeployEngine = class {
36164
37095
  }
36165
37096
  let outputs;
36166
37097
  try {
36167
- outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
37098
+ outputs = await withSharedDrainBudget(() => this.resolveOutputs(template, newResources, stackName, outputsDigestSource, parameterValues, conditions));
36168
37099
  const resolvedOutputsBeforeRedaction = outputs;
36169
37100
  outputs = this.redactOutputs(outputs);
36170
37101
  this.rememberRecoverableMaskedOutputs(stackName, resolvedOutputsBeforeRedaction, outputs);
@@ -36181,6 +37112,7 @@ var DeployEngine = class {
36181
37112
  resources: newResources,
36182
37113
  outputs,
36183
37114
  exportNames: [...this.resolvedExportNames],
37115
+ ...this.skippedOutputs && { skippedOutputs: { ...this.skippedOutputs } },
36184
37116
  ...this.recordedImports.length > 0 && { imports: [...this.recordedImports] },
36185
37117
  ...this.recordedOutputReads.length > 0 && { outputReads: [...this.recordedOutputReads] },
36186
37118
  lastModified: Date.now()
@@ -36219,6 +37151,7 @@ var DeployEngine = class {
36219
37151
  resources: newResources,
36220
37152
  outputs: currentState.outputs,
36221
37153
  ...exportNamesCarriedFrom(currentState),
37154
+ ...skippedOutputsCarriedFrom(currentState),
36222
37155
  ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
36223
37156
  lastModified: Date.now()
36224
37157
  });
@@ -37360,9 +38293,16 @@ var DeployEngine = class {
37360
38293
  * opposite sides of the same question. `secrets` is the outputs pass's own
37361
38294
  * map: everything recorded before this handler runs, an `Export.Name`
37362
38295
  * resolution's entries included (its `finally` merges them back before the
37363
- * `catch` reaches here). What a still-pending concurrent part would have
37364
- * recorded is outside both issue #2563's late write, the same bound every
37365
- * other masking site in this engine has.
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.)
37366
38306
  *
37367
38307
  * The strict arm's `cause` is masked as an OBJECT, through
37368
38308
  * `maskSecretsInError` — a clone of each `Error` link `errorCauseChain`
@@ -37399,10 +38339,10 @@ var DeployEngine = class {
37399
38339
  * rather than whatever the declaration order happened to have recorded by
37400
38340
  * then (issue #1919).
37401
38341
  */
37402
- async resolveOutputs(template, resources, stackName, parameterValues, conditions) {
38342
+ async resolveOutputs(template, resources, stackName, digestSource, parameterValues, conditions) {
37403
38343
  this.resolvedExportNames = [];
37404
38344
  if (!template.Outputs) return {};
37405
- const outputs = {};
38345
+ const outputs = Object.create(null);
37406
38346
  const context = this.buildResolverContext({
37407
38347
  template,
37408
38348
  resources,
@@ -37447,8 +38387,8 @@ var DeployEngine = class {
37447
38387
  }
37448
38388
  if (typeof exportName !== "string") continue;
37449
38389
  const exposure = exportNameSecretExposure(exportName, nameSecrets, context.recordedSecretValues);
37450
- if (exposure) this.logger.warn(secretBearingExportNameWarning(outputKey, exportName, exposure));
37451
- 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));
37452
38392
  else {
37453
38393
  outputs[exportName] = value;
37454
38394
  if (!this.resolvedExportNames.includes(exportName)) this.resolvedExportNames.push(exportName);
@@ -37467,6 +38407,7 @@ var DeployEngine = class {
37467
38407
  if (!publishedOutputNames.has(outputKey)) continue;
37468
38408
  this.outputsTemplateSource[outputKey] = output.Value;
37469
38409
  }
38410
+ this.skippedOutputs = collectSkippedOutputs(digestSource, outputs);
37470
38411
  return markSameGenerationBag(outputs);
37471
38412
  }
37472
38413
  buildDisplayOutputs(template, resolvedOutputs) {
@@ -37481,5 +38422,5 @@ var DeployEngine = class {
37481
38422
  };
37482
38423
 
37483
38424
  //#endregion
37484
- export { createMaskedRetryLogger as $, runDockerStreaming as $n, StackTerminationProtectionError as $r, carriesSecretMask as $t, WARM_THROUGHPUT_MEMBERS as A, rewriteTemplateAssetReferences as An, clearBucketRegionCache as Ar, replayWarn as At, yellow as B, validateContainerRepoName as Bn, DependencyError as Br, s3BucketWebsiteUrl as Bt, unsupportedFinalSnapshotError as C, shouldRetainResource as Cn, displaySafe as Cr, normalizeAwsTagsToCfn as Ct, isStatefulRecreateTargetForReplace as D, buildAssetRedirectMap as Dn, derivePartitionAndUrlSuffix as Dr, configBooleanRefusal as Dt, MULTI_REGION_RECREATE_BLOCKED_TYPES as E, WorkGraph as En, canonicalizeRegion as Er, coerceCfnBoolean as Et, bold as F, getBootstrapMarkerKey as Fn, setAwsClients as Fr, producerRegionsFromState as Ft, secretBearingStateKeyWarning as G, describeDockerExecFailure as Gn, LocalStartServiceError as Gr, findSilentDropProperties as Gt, collectPublishedOutputNames as H, describeAwsFailure as Hn, DynamicReferenceRegionAmbiguousError as Hr, DiffCalculator as Ht, cyan as I, isCrossRegionRedirect as In, AssetError as Ir, s3BucketArn as It, IAMRoleProvider as J, formatDockerLoginError as Jn, PartialFailureError as Jr, DagBuilder as Jt, stateKeySecretExposure as K, describeDockerFailure as Kn, LockError as Kr, describeTypeWithThrottleRetry as Kt, gray as L, parseBootstrapMarker as Ln, CdkdError as Lr, s3BucketDomainName as Lt, isWarmThroughputDecrease as M, BOOTSTRAP_MARKER_PREFIX as Mn, AwsClients as Mr, requireConfigObject as Mt, toFiniteNumber as N, assertAssetBucketRegion as Nn, getAwsClients as Nr, requireConfigString as Nt, isStatefulRecreateTargetSync as O, createAssetRedirectResolver as On, AssemblyReader as Or, configStringRefusal as Ot, formatResourceLine as P, ensureAssetStorage as Pn, resetAwsClients as Pr, classifyReplaySecretRegion as Pt, wouldReturnToSdkProvider as Q, runDockerForeground as Qn, StackHasActiveImportsError as Qr, TEMPLATE_SOURCED_RULES as Qt, green as R, readBootstrapMarkerBody as Rn, ConfigError as Rr, s3BucketDualStackDomainName as Rt, refusesFinalSnapshot as S, importableOutputs as Sn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as Sr, WAFv2WebACLProvider as St, extractDeploymentEventError as T, stringifyValue as Tn, PARTITION_TABLE as Tr, assertRegionMatch as Tt, exportAliasCollisionScrubWarning as U, buildDockerImage as Un, IntrinsicResolutionRefusalError as Ur, INTRINSIC_KEYS as Ut, collectDeclaredOutputNames as V, buildDenyExternalAccessPolicy as Vn, DeployCancelledError as Vr, applyRoleArnIfSet as Vt, isExportAliasCollision as W, describeDockerCapturedOutput as Wn, LocalInvokeBuildError as Wr, findActionableSilentDrops as Wt, clearOnUpdateRemoval as X, partitionSensitiveEnv as Xn, ResourceTimeoutError as Xr, STATE_SOURCED_CROSS_GENERATION_RULES as Xt, collectInlinePolicyNamesManagedBySiblings as Y, getDockerCmd as Yn, ProvisioningError as Yr, TemplateParser as Yt, ProviderRegistry as Z, redactDockerArgvValues as Zn, ResourceUpdateNotSupportedError as Zr, STATE_SOURCED_READBACK_RULES as Zt, PRE_DELETE_SNAPSHOT_TYPES as _, shellQuote as _n, CFN_TEMPLATE_BODY_LIMIT as _r, coerceParameterTypedValue as _t, DeploymentEventsStore as a, withErrorHandling as ai, maskSecretsInText as an, synthesisStatusMessage as ar, isInterruptedWaitError as at, createPreDeleteFinalSnapshot as b, exportNamesCarriedFrom as bn, findLargeInlineResources as br, parameterTypeMayLoseSecretIdentity as bt, replayFailedOperations as c, isThrottlingError as ci, redactSecretsForState as cn, resolveApp as cr, slowCcOperationTimeoutMs as ct, updatePartialReason as d, markRedactedCause as di, S3StateBackend as dn, resolveSkipPrefix as dr, deleteSkipReason as dt, StateError as ei, createSecretMasker as en, escapeRegExp$1 as er, maskDeep as et, withResourceDeadline as f, retryClassificationText as fi, rebuildClientForBucketRegion as fn, resolveStateBucketWithDefault as fr, disableInstanceApiTermination as ft, ATOMIC_FINAL_SNAPSHOT_TYPES as g, forceQuitRecoveryClause as gn, warnDeprecatedNoPrefixCliFlag as gr, cfnRefValueFromPhysicalId as gt, computeImplicitDeleteEdges as h, buildLockContentionMessage as hn, stateBucketExistenceConfirmed as hr, carriesDynamicReference as ht, DeploymentEventsReader as i, normalizeAwsError as ii, maskSecretsInError as in, Synthesizer as ir, interruptWatchListenerCount as it, coerceWarmThroughput as j, AssetModeResolver as jn, resolveBucketRegion as jr, requireConfigArray as jt, renderStatefulReason as k, loadPublishableAssetManifest as kn, processStackMessages as kr, readConfigString as kt, replayRollback as l, isTransientServerError as li, scrubResourceRecord as ln, resolveAutoAssetStorage as lr, UNSPECIFIED_SKIP_REASON as lt, IMPLICIT_DELETE_DEPENDENCIES as m, buildForceUnlockCommand as mn, resolveUseCdkBootstrapAssets as mr, IntrinsicFunctionResolver as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, formatError as ni, errorCauseChain as nn, AssetManifestLoader as nr, beginCommandInterruptScope as nt, planFailedOps as o, isMarkedNonRetryable as oi, recordMaskOnlyValue as on, getDefaultStateBucketName as or, startInterruptWatch as ot, maskingRetryLogger as p, __exportAll as pi, UNRENDERABLE as pn, resolveStateBucketWithDefaultAndSource as pr, isTerminationProtectionPropagationError as pt, getCurrentResourceSecrets as q, dockerSpawnEnvWithSensitive as qn, NestedStackChildDirectDestroyError as qr, withRetry as qt, DeployEngine as r, isCdkdError as ri, isSingleDynamicReferenceToken as rn, getDockerImageBySourceHash as rr, endCommandInterruptScope as rt, planRollback as s, isRetryableTransientError as si, recoverMaskedOutput as sn, getLegacyStateBucketName as sr, CloudControlProvider as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, SynthesisError as ti, dynamicReferenceTokens as tn, stripControlChars as tr, maskerOrIdentity as tt, updatePartialMessage as u, markNonRetryable as ui, LockManager as un, resolveCaptureObservedState as ur, deleteIndeterminateGuards as ut, buildFinalSnapshotIdentifier as v, CUSTOM_RESOURCE_RESPONSE_PREFIX as vn, CFN_TEMPLATE_URL_LIMIT as vr, getAccountInfo as vt, makeCanonicalizePropertiesFn as w, AssetPublisher as wn, expectedOwnerParam as wr, resolveExplicitPhysicalId as wt, isFinalSnapshotError as x, importableOutputKeys as xn, uploadCfnTemplate as xr, refStateLookupFromResource as xt, ccRoutedFinalSnapshotError as y, DEFAULT_STATE_PREFIX as yn, MIGRATE_TMP_PREFIX as yr, isUnboundTemplateParameter as yt, red as z, validateAssetBucketName as zn, CrossAccountSecretRefusalError as zr, s3BucketRegionalDomainName as zt };
37485
- //# sourceMappingURL=deploy-engine-Cxt3gn8N.js.map
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