@go-to-k/cdkd 0.288.1 → 0.288.3

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-CGFRodgE.js";
3
+ import { t as getCdkdVersion } from "./version-CEyZvTcB.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";
@@ -2337,7 +2337,7 @@ async function resolveCrossAccountStateBucket(accountId, credentials) {
2337
2337
  /** Cloud assembly schema version compatible with CDK v2 */
2338
2338
  const CDK_ASM_VERSION = "38.0.0";
2339
2339
  /** Maximum context size before overflow to temp file (32KB) */
2340
- const CONTEXT_OVERFLOW_LIMIT = 32 * 1024;
2340
+ const CONTEXT_OVERFLOW_LIMIT = 32768;
2341
2341
  /**
2342
2342
  * Remove one matched pair of surrounding quotes from a shell token, so a quoted
2343
2343
  * entrypoint (`"bin/app.js"`) is recognized as a .js file and re-quoted cleanly.
@@ -2551,7 +2551,6 @@ function processStackMessages(stacks, logger, options = {}) {
2551
2551
  case "error":
2552
2552
  hasErrors = true;
2553
2553
  logger.error(`[Error at ${msg.path}] ${msg.message}`);
2554
- break;
2555
2554
  }
2556
2555
  const failAt = options.strict ? "warn" : options.ignoreErrors ? "none" : "error";
2557
2556
  if (hasErrors && failAt !== "none") throw new SynthesisError("Found errors");
@@ -3174,10 +3173,7 @@ var CcApiContextProvider = class {
3174
3173
  case "at-least-one":
3175
3174
  if (count < 1) throw new Error(`Expected at least one ${typeName}${context}, found none`);
3176
3175
  break;
3177
- case "at-most-one":
3178
- if (count > 1) throw new Error(`Expected at most one ${typeName}${context}, found ${count}`);
3179
- break;
3180
- case "any": break;
3176
+ case "at-most-one": if (count > 1) throw new Error(`Expected at most one ${typeName}${context}, found ${count}`);
3181
3177
  }
3182
3178
  }
3183
3179
  /**
@@ -4514,7 +4510,8 @@ async function purgeNoncurrentKeyVersions(s3Client, bucket, keys, options = {})
4514
4510
  const elided = failed.size - named.length;
4515
4511
  logger.warn(`Could not purge noncurrent versions of ${failed.size} key(s) in s3://${displaySafe(bucket, { asciiOnly: true })}. Their previous versions survive and remain readable via GetObject with a VersionId (${options.objectDescription ?? "the body of an object cdkd has just reported as removed"}). Grant s3:ListBucketVersions and s3:DeleteObjectVersion on the state bucket, or purge the key(s) by hand. Failures: ${named.join(", ")}` + (elided > 0 ? ` (and ${elided} more)` : ""));
4516
4512
  }
4517
- await warnIfPurgeIsReplicated(s3Client, bucket, [.../* @__PURE__ */ new Set([...purged, ...unsettledBodies])], {
4513
+ const replicationKeys = [.../* @__PURE__ */ new Set([...purged, ...unsettledBodies])];
4514
+ await warnIfPurgeIsReplicated(s3Client, bucket, replicationKeys, {
4518
4515
  requestFields,
4519
4516
  logger,
4520
4517
  ...options.objectDescription !== void 0 && { objectDescription: options.objectDescription }
@@ -5114,7 +5111,8 @@ async function expandMacrosAttempt(template, opts, logger) {
5114
5111
  ChangeSetName: changeSetName
5115
5112
  })).catch(() => void 0);
5116
5113
  const reason = desc?.StatusReason ?? "unknown (DescribeChangeSet failed)";
5117
- throw new MacroExpansionError(`CloudFormation macro expansion failed (status=${desc?.Status ?? "UNKNOWN"}): ${reason}`, waiterError instanceof Error ? waiterError : void 0);
5114
+ const status = desc?.Status ?? "UNKNOWN";
5115
+ throw new MacroExpansionError(`CloudFormation macro expansion failed (status=${status}): ${reason}`, waiterError instanceof Error ? waiterError : void 0);
5118
5116
  }
5119
5117
  const tpl = await cfn.send(new GetTemplateCommand({
5120
5118
  StackName: transientStackName,
@@ -5123,7 +5121,10 @@ async function expandMacrosAttempt(template, opts, logger) {
5123
5121
  }));
5124
5122
  if (tpl.TemplateBody === void 0 || tpl.TemplateBody === null) throw new MacroExpansionError(`CloudFormation returned no Processed-stage template body for the macro-expansion changeset. This typically indicates a CFn-side regression — re-run, and if the failure persists open an issue with the transforms involved: [${macros.join(", ")}].`);
5125
5123
  const expanded = parseTemplateBody(tpl.TemplateBody);
5126
- if (containsMacro(expanded)) throw new MacroExpansionError(`Macro expansion produced a template that still contains macros [${enumerateMacros(expanded).join(", ")}]. Multi-stage macros (a macro whose expansion emits another macro reference) are intentionally out of scope in cdkd v1 — see https://github.com/go-to-k/cdkd/issues/463. If you need this pattern, manually pre-expand the template and deploy the result.`);
5124
+ if (containsMacro(expanded)) {
5125
+ const inner = enumerateMacros(expanded);
5126
+ throw new MacroExpansionError(`Macro expansion produced a template that still contains macros [${inner.join(", ")}]. Multi-stage macros (a macro whose expansion emits another macro reference) are intentionally out of scope in cdkd v1 — see https://github.com/go-to-k/cdkd/issues/463. If you need this pattern, manually pre-expand the template and deploy the result.`);
5127
+ }
5127
5128
  logger.debug(`Macro expansion: success — ${Object.keys(expanded.Resources ?? {}).length} resources after expansion.`);
5128
5129
  return expanded;
5129
5130
  } finally {
@@ -5245,7 +5246,8 @@ function loadJsonConfig(filePath) {
5245
5246
  * Load cdk.json from the current working directory
5246
5247
  */
5247
5248
  function loadCdkJson(cwd) {
5248
- return loadJsonConfig(resolve(cwd || process.cwd(), "cdk.json"));
5249
+ const dir = cwd || process.cwd();
5250
+ return loadJsonConfig(resolve(dir, "cdk.json"));
5249
5251
  }
5250
5252
  /**
5251
5253
  * Load user-level defaults from ~/.cdk.json
@@ -6200,8 +6202,10 @@ async function spawnStreaming(cmd, args, options = {}) {
6200
6202
  const stderrChunks = [];
6201
6203
  child.stdout.on("data", (chunk) => {
6202
6204
  stdoutChunks.push(chunk);
6203
- if (streamLive) if (isStdoutReservedForPayload()) process.stderr.write(chunk);
6204
- else process.stdout.write(chunk);
6205
+ if (streamLive) {
6206
+ if (isStdoutReservedForPayload()) process.stderr.write(chunk);
6207
+ else process.stdout.write(chunk);
6208
+ }
6205
6209
  });
6206
6210
  child.stderr.on("data", (chunk) => {
6207
6211
  stderrChunks.push(chunk);
@@ -7831,10 +7835,11 @@ async function assertAssetBucketRegion(s3Client, bucketName, expectedRegion, acc
7831
7835
  const fromHeader = readBucketRegionHeader(cause);
7832
7836
  if (fromHeader) actual = canonicalizeRegion(fromHeader);
7833
7837
  else try {
7834
- actual = canonicalizeRegion(bucketLocationToRegion$1((await s3Client.send(new GetBucketLocationCommand({
7838
+ const location = await s3Client.send(new GetBucketLocationCommand({
7835
7839
  Bucket: bucketName,
7836
7840
  ExpectedBucketOwner: accountId
7837
- }))).LocationConstraint));
7841
+ }));
7842
+ actual = canonicalizeRegion(bucketLocationToRegion$1(location.LocationConstraint));
7838
7843
  } catch (probeError) {
7839
7844
  const fromProbe = readBucketRegionHeader(probeError);
7840
7845
  if (fromProbe) actual = canonicalizeRegion(fromProbe);
@@ -9000,7 +9005,8 @@ var AssetPublisher = class {
9000
9005
  const err = error;
9001
9006
  const message = stringifyValue(err["message"] || err["name"] || error);
9002
9007
  const code = stringifyValue(err["Code"] || err["code"] || err["name"] || "");
9003
- throw new AssetError(`Asset publishing failed: ${code ? `${code}: ${message}` : message}`, error instanceof Error ? error : void 0);
9008
+ const detail = code ? `${code}: ${message}` : message;
9009
+ throw new AssetError(`Asset publishing failed: ${detail}`, error instanceof Error ? error : void 0);
9004
9010
  }
9005
9011
  }
9006
9012
  /**
@@ -9095,6 +9101,18 @@ function importableOutputs(state) {
9095
9101
  function exportNamesCarriedFrom(previous) {
9096
9102
  return previous.exportNames === void 0 ? {} : { exportNames: previous.exportNames };
9097
9103
  }
9104
+ /**
9105
+ * The `skippedOutputs` field to write when a save carries the PREVIOUS
9106
+ * record's `outputs` bag forward instead of re-resolving it (issue #2740). The
9107
+ * record describes that bag — which keys the deploy that wrote it could not
9108
+ * resolve — so it travels with the bag, exactly like {@link
9109
+ * exportNamesCarriedFrom}: absent stays absent, present stays as it was. A
9110
+ * writer that re-resolves outputs does NOT call this; it writes the set the
9111
+ * resolution produced, or omits the field when nothing was skipped.
9112
+ */
9113
+ function skippedOutputsCarriedFrom(previous) {
9114
+ return previous.skippedOutputs === void 0 ? {} : { skippedOutputs: previous.skippedOutputs };
9115
+ }
9098
9116
 
9099
9117
  //#endregion
9100
9118
  //#region src/types/rollback-journal.ts
@@ -10184,9 +10202,10 @@ var S3StateBackend = class {
10184
10202
  if (isNoSuchKey(error)) return { kind: "absent" };
10185
10203
  const { detail } = describeAwsFailure(error);
10186
10204
  this.logger.debug(`Could not read legacy state region for '${this.displayName(stackName)}': ${displaySafe(detail, { asciiOnly: true }) || "<unrenderable>"}`);
10205
+ const cls = error instanceof Error && error.name ? error.name : "an unknown error";
10187
10206
  return {
10188
10207
  kind: "unreadable",
10189
- reason: displaySafe(error instanceof Error && error.name ? error.name : "an unknown error", { asciiOnly: true }) || "<unrenderable>"
10208
+ reason: displaySafe(cls, { asciiOnly: true }) || "<unrenderable>"
10190
10209
  };
10191
10210
  }
10192
10211
  }
@@ -10289,7 +10308,7 @@ function isNoSuchKey(error) {
10289
10308
  * tolerates FOURTEEN consecutive missed renewals before it lapses, and an
10290
10309
  * ordinary broad-set integ (~8 min) performs three.
10291
10310
  */
10292
- const MAX_RENEWAL_INTERVAL_MS = 120 * 1e3;
10311
+ const MAX_RENEWAL_INTERVAL_MS = 12e4;
10293
10312
  /**
10294
10313
  * Renew after at most a quarter of the TTL, so a short TTL still gets three
10295
10314
  * chances to renew before it lapses. This is what binds the two numbers
@@ -11093,7 +11112,9 @@ var LockManager = class {
11093
11112
  const expiresIn = lockInfo ? this.formatDuration(lockInfo.expiresAt - Date.now()) : "unknown";
11094
11113
  const forceUnlockCommand = buildForceUnlockCommand(stackName, region);
11095
11114
  const recovery = forceUnlockCommand ? `If you are certain no other process is active, run: ${forceUnlockCommand}` : `If you are certain no other process is active, ${UNREPRODUCIBLE_LOCK_CLAUSE.charAt(0).toLowerCase()}${UNREPRODUCIBLE_LOCK_CLAUSE.slice(1)}`;
11096
- throw new LockError(`Failed to acquire lock for stack '${displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>"}' (${displaySafe(region, { asciiOnly: true }) || "<unrenderable>"}) after ${maxRetries + 1} attempts. ` + (lockInfo ? `Locked by: ${lockInfo.owner}${lockInfo.operation ? `, operation: ${lockInfo.operation}` : ""}, expires in: ${expiresIn}. ` + recovery : `Lock exists but could not read lock info. ${recovery}`));
11115
+ const safeStack = displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>";
11116
+ const safeRegion = displaySafe(region, { asciiOnly: true }) || "<unrenderable>";
11117
+ throw new LockError(`Failed to acquire lock for stack '${safeStack}' (${safeRegion}) after ${maxRetries + 1} attempts. ` + (lockInfo ? `Locked by: ${lockInfo.owner}${lockInfo.operation ? `, operation: ${lockInfo.operation}` : ""}, expires in: ${expiresIn}. ` + recovery : `Lock exists but could not read lock info. ${recovery}`));
11097
11118
  }
11098
11119
  };
11099
11120
 
@@ -11197,6 +11218,102 @@ function resolvedPlaintextOf(secrets, expression) {
11197
11218
  return typeof recorded === "string" ? recorded : void 0;
11198
11219
  }
11199
11220
  /**
11221
+ * The bag OBJECTS a deploy pass produced ITSELF and installed on a success
11222
+ * path — the fact the resolved-pair evidence above cannot state (issue
11223
+ * [#2516](https://github.com/go-to-k/cdkd/issues/2516)).
11224
+ *
11225
+ * A pair proves that THIS pass resolved a token to a plaintext; it says nothing
11226
+ * about which bag is being walked. The deploy engine's persist choke point
11227
+ * walks EVERY record in the state map against today's template, and a record
11228
+ * that merely ENTERED the create/update arm is still the PREVIOUS generation
11229
+ * until its provider call succeeds (an intermediate save, a pre/post-rollback
11230
+ * save, Ctrl-C — the same population the `sourceIsSameGeneration` note on
11231
+ * {@link PathSourceRules} names). One record also carries two bags of
11232
+ * different provenance: `properties`, this pass's resolved bag once the
11233
+ * provider succeeded, and `observedProperties`, an AWS readback installed
11234
+ * separately. So the fact is BAG-specific, not caller- or record-level, and it
11235
+ * is carried on the object: {@link markSameGenerationBag} at the moment the
11236
+ * successful result is installed, consulted by {@link redactSecretsForState}
11237
+ * for the object it is handed. A copy of the bag, a derived needle map, a
11238
+ * previous generation's record, a scrub / import / drift walk and a bag the
11239
+ * engine did not mark all answer `false` and keep the fall-through.
11240
+ *
11241
+ * A `WeakSet` for the reason {@link resolvedPairsOf} is a `WeakMap`: the mark
11242
+ * dies with the object it is on, and nothing has to clear it.
11243
+ */
11244
+ const sameGenerationBags = /* @__PURE__ */ new WeakSet();
11245
+ /**
11246
+ * Mark `bag` as an object THIS pass is entitled to speak for, and return it.
11247
+ * The one evidence {@link positionByEmbeddedSpan} needs beyond a resolved pair
11248
+ * to write a middle SHORTER than {@link MIN_NEEDLE_LENGTH} — see the arm.
11249
+ *
11250
+ * Deliberately NOT "produced by resolving today's template, read back from a
11251
+ * resource it just wrote, and about to be installed on a success path": each
11252
+ * of those three was in this sentence and each is false at one site (an
11253
+ * unchanged resource's auto-refresh resolves nothing; the no-change re-check
11254
+ * and the journal are never installed on the record). The conditions are per
11255
+ * site and are listed below.
11256
+ *
11257
+ * WHAT THE MARK CLAIMS, which is the only thing every site shares: the object
11258
+ * is one THIS PASS is entitled to speak for, so a sub-floor middle in it may
11259
+ * be written as the token this pass recorded. What never qualifies is a bag of
11260
+ * MIXED provenance: a provider's `effectiveProperties` replacement may carry
11261
+ * previous-state values IN, so an object-level mark on it would prove nothing
11262
+ * for those leaves — it stays unmarked and keeps the residual. Nor does the
11263
+ * object the resolver produced, marked at resolution time: it is not
11264
+ * necessarily the object state ends up holding.
11265
+ *
11266
+ * The CONDITIONS are per SITE, not global, and stating them globally is what
11267
+ * this paragraph kept getting wrong (PR 2753, rounds 3 and 4). "After the
11268
+ * provider call" is false for the no-change re-check, which marks BEFORE it
11269
+ * and may skip it entirely. "Every leaf this pass produced" is false for the
11270
+ * auto-refresh readback of an UNCHANGED resource, where nothing was resolved
11271
+ * at all — there the safety comes from the empty secrets map, not from the
11272
+ * mark. Read the per-site list below rather than a rule over all five.
11273
+ *
11274
+ * The five call sites, and which of them the record HOLDS, because the earlier
11275
+ * "two never-installed copies" reading of this paragraph was false once the
11276
+ * third copy arrived:
11277
+ * - `propertiesToRecord` — the record's `properties`, installed. The resolved
11278
+ * bag itself when the route drops nothing, a narrowed copy of it when it
11279
+ * does, and `withoutSilentDropProperties` returns its input by reference in
11280
+ * the first case, which is why the mark must be taken AFTER the narrowing
11281
+ * and not before.
11282
+ * - `drainObservedCaptures` — the `observedProperties` readback it installs,
11283
+ * and a COPY: the object a provider returned is never the marked one, so a
11284
+ * provider that hands back its own `properties` argument cannot get a
11285
+ * previous generation marked. It marks EVERY capture it drains, which is
11286
+ * wider than "a resource this pass wrote" — the schema-upgrade auto-refresh
11287
+ * of an UNCHANGED resource is marked too. What keeps that safe is the map,
11288
+ * not the mark: `perResourceSecrets` is populated only in the create /
11289
+ * update arms, so an unchanged resource's walk carries an empty one and the
11290
+ * span arm cannot fire whatever the object is marked.
11291
+ * - `resolveOutputs` — the `outputs` bag this pass resolved. What every site
11292
+ * shares is that the marked object is the redaction INPUT; whether the
11293
+ * record then holds that same object varies, and here it depends on whether
11294
+ * this pass recorded any output secret: with one,
11295
+ * `redactOutputs` returns a fresh redacted bag and that return value is
11296
+ * installed; with none — the ordinary deploy — it returns its input
11297
+ * unchanged and the marked object IS the one
11298
+ * stored. On the no-change path the newly redacted bag is installed when
11299
+ * the outputs CHANGED, and the previous `persistedOutputs` is kept
11300
+ * otherwise; that previous bag is unmarked, which is the answer that arm
11301
+ * wants.
11302
+ * - the update arm's no-change re-check — a marked `{ ...resolvedProps }`
11303
+ * compared against the stored record so a stored token reads as a no-op.
11304
+ * NOT installed; the object the provider is handed is the unmarked original.
11305
+ * - the rollback journal's FAILED-op `attemptedProperties` — a marked copy, so
11306
+ * that persisted artifact does not carry the plaintext on exactly the
11307
+ * failure path. NOT installed on the record.
11308
+ */
11309
+ function markSameGenerationBag(bag) {
11310
+ sameGenerationBags.add(bag);
11311
+ return bag;
11312
+ }
11313
+ function isSameGenerationBag(bag) {
11314
+ return sameGenerationBags.has(bag);
11315
+ }
11316
+ /**
11200
11317
  * Every `{{resolve:...}}` expression this process has PROVEN resolves to a
11201
11318
  * secret, as a SET — uncollapsed by resolved value (issue #1910).
11202
11319
  *
@@ -12847,15 +12964,78 @@ function singleSpanFrame(bag, source) {
12847
12964
  * not create is the whole-token arm's: a middle that is ALREADY an expression
12848
12965
  * (a persisted answer from another generation), which the token refusal below
12849
12966
  * keeps out — and, by the same argument, any leaf the value scan would NOT
12850
- * rewrite to exactly `prefix + survivor + suffix`: a middle shorter than the
12851
- * scan's needle floor (an embedded 1-3 character secret stays the scan's
12852
- * documented residualissue #2516 tracks closing it with a bound that
12853
- * proves the bag's generation, which this evidence does not), a whole leaf
12854
- * that is itself another recorded plaintext, a needle starting in the prefix
12855
- * and overlapping the middle. The
12856
- * arm checks that equivalence against the scan's own answer rather than
12857
- * re-deriving the scan's rules. Pinned by the cross-generation cases in
12858
- * `secret-redaction-embedded-span.test.ts`.
12967
+ * rewrite to exactly `prefix + survivor + suffix`: a whole leaf that is itself
12968
+ * another recorded plaintext, a needle starting in the prefix and overlapping
12969
+ * the middle, and on a bag whose generation is NOT proven — a middle shorter
12970
+ * than the scan's needle floor. The arm checks that equivalence against the
12971
+ * scan's own answer rather than re-deriving the scan's rules. Pinned by the
12972
+ * cross-generation cases in `secret-redaction-embedded-span.test.ts`.
12973
+ *
12974
+ * BELOW THE NEEDLE FLOOR the scan makes no claim at all (issue
12975
+ * [#2516](https://github.com/go-to-k/cdkd/issues/2516)): {@link buildNeedleRegex}
12976
+ * drops every plaintext shorter than {@link MIN_NEEDLE_LENGTH} from its
12977
+ * alternation, so an embedded 1-3 character secret is left in plaintext by
12978
+ * the scan, with or without a same-plaintext sibling. Accepting the scan's
12979
+ * silence (`scanned === bag`) as equivalence would therefore be a NEW claim
12980
+ * rather than a choice within the scan's class, and on a previous
12981
+ * generation's bag it would fabricate: a 1-3 character readback or old
12982
+ * record that COINCIDES with today's plaintext (`port:0` where AWS returns a
12983
+ * default and today's secret resolved to `0`) would be persisted as today's
12984
+ * expression, which round-trips today and, after a rotation, reports a drift
12985
+ * that never happened. So that arm is admitted only for a bag whose
12986
+ * generation IS proven: `bagIsSameGeneration`, the object-level mark
12987
+ * {@link markSameGenerationBag} puts on the bags the deploy engine hands to
12988
+ * redaction on a success path (the record's `properties`, which are the
12989
+ * resolved bag or the subset of it the SDK route writes; EVERY
12990
+ * `observedProperties` readback `drainObservedCaptures` drains, an unchanged
12991
+ * resource's auto-refresh included, where the empty secrets map rather than
12992
+ * the mark is what keeps it safe; and the `outputs` bag this pass resolved,
12993
+ * whose redacted RETURN VALUE is usually what the record holds) and on two
12994
+ * copies of the resolver's own output that the record never holds. That
12995
+ * function's contract is the authority — the conditions differ per site and
12996
+ * do not survive being stated once. Threaded down from
12997
+ * {@link redactSecretsForState} for exactly the object it was handed. With
12998
+ * the mark AND the pair, `recorded === middle` says this pass resolved the
12999
+ * source token to the middle and the bag is this pass's own, so
13000
+ * `prefix + token + suffix` is what the template says at that leaf and what
13001
+ * the resource holds. Without the mark the sub-floor middle keeps today's
13002
+ * bound, which is the scan's answer: the plaintext, unchanged.
13003
+ *
13004
+ * What stays, stated here rather than papered over. A provider that
13005
+ * substituted `effectiveProperties` built a bag of MIXED provenance, so that
13006
+ * object is never marked and a sub-floor middle in it keeps the scan's
13007
+ * answer. A readback AWS rewrote at that offset to a value that coincides
13008
+ * with the 1-3 character secret is persisted as the expression: value-equal
13009
+ * until the secret rotates, and the same residual the value scan already has
13010
+ * for a 4+ character coincidence in a readback. Every refusal of this arm
13011
+ * returns the scan's answer, so a leaf refused for interference (a frame
13012
+ * that is itself a recorded plaintext, say) has its frame rewritten and its
13013
+ * sub-floor middle left in plaintext. And this arm positions a LITERAL
13014
+ * source leaf only: an `Fn::Join` / `Fn::Sub` source rendering the same
13015
+ * `port:` + token goes to {@link positionByIntrinsicSkeleton}, which refuses
13016
+ * unless the WHOLE bag is a recorded plaintext, so a sub-floor secret
13017
+ * embedded through an intrinsic — the dominant CDK shape — still falls to
13018
+ * the value scan and persists in plaintext. `cdkd scrub`, the documented
13019
+ * repair tool for a pre-GHSA record, cannot repair a sub-floor embedded leaf
13020
+ * either: it walks a STORED bag, which no deploy marked, so the arm is
13021
+ * unreachable from it by construction and the leaf keeps the scan's answer.
13022
+ * The MASKING channel keeps the residual whole: `maskSecretsInText`'s
13023
+ * substring arm carries the same four-character floor, so once this arm has
13024
+ * put `port:{{resolve:...}}` in state, a warn line quoting an AWS message can
13025
+ * still print `port:q7`. Pre-existing and not a regression -- the #2453 class,
13026
+ * and the reason the list would otherwise read as complete when it is not
13027
+ * (maintainer review of PR 2753, round 2).
13028
+ *
13029
+ * The next DEPLOY of that resource repairs the leaves this arm is eligible
13030
+ * for — a LITERAL source leaf on a bag the engine marks — because the
13031
+ * re-check compares unequal against a record holding the plaintext and the
13032
+ * resource is written again from a bag this pass produced. The residuals
13033
+ * named above (an `effectiveProperties` substitution, an intrinsic source
13034
+ * shape) are not repaired by that deploy either; they stay tracked by
13035
+ * #2745. Tracked, with the sub-floor
13036
+ * residuals of a nested-stack child's inherited parameter and of `cdkd
13037
+ * import`'s own resolution, by issue
13038
+ * [#2745](https://github.com/go-to-k/cdkd/issues/2745).
12859
13039
  *
12860
13040
  * One shape reaches this arm that a reader may not expect: a WHOLE-token
12861
13041
  * source that FAILED the whole-token arm's `isKnownSecretExpression` gate (an
@@ -12883,7 +13063,7 @@ function singleSpanFrame(bag, source) {
12883
13063
  * the scan as a parameter, which left the bound one wrong caller away from
12884
13064
  * comparing against a scan of some other bag with no type error.
12885
13065
  */
12886
- function positionByEmbeddedSpan(bag, source, secrets) {
13066
+ function positionByEmbeddedSpan(bag, source, secrets, bagIsSameGeneration) {
12887
13067
  const scanned = redactSecretsForState(bag, secrets);
12888
13068
  const frame = singleSpanFrame(bag, source);
12889
13069
  if (frame === void 0) return scanned;
@@ -12892,8 +13072,9 @@ function positionByEmbeddedSpan(bag, source, secrets) {
12892
13072
  if (recorded === void 0 || recorded !== middle) return scanned;
12893
13073
  const survivor = secrets.get(middle);
12894
13074
  if (survivor === void 0) return scanned;
12895
- if (scanned !== prefix + survivor + suffix) return scanned;
12896
- return prefix + token + suffix;
13075
+ if (scanned === prefix + survivor + suffix) return prefix + token + suffix;
13076
+ if (bagIsSameGeneration && scanned === bag) return prefix + token + suffix;
13077
+ return scanned;
12897
13078
  }
12898
13079
  /**
12899
13080
  * Keys tried, in order, when pairing two arrays whose ORDER cannot be trusted
@@ -13089,13 +13270,13 @@ function positionListByCrossStackSource(bag, source, secrets) {
13089
13270
  * association where the position is a cross-stack read, skeleton where it is a
13090
13271
  * describable intrinsic, value where none is.
13091
13272
  */
13092
- function redactByPath(bag, source, secrets, rules, secretExpressions) {
13273
+ function redactByPath(bag, source, secrets, rules, secretExpressions, bagIsSameGeneration) {
13093
13274
  if (isDynamicReferenceString(source) && typeof bag === "string") {
13094
13275
  if (isSingleDynamicReferenceToken(source) && (rules.trustAnyExpression || isKnownSecretExpression(source, secretExpressions))) {
13095
13276
  if (!rules.sourceIsSameGeneration && isSingleDynamicReferenceToken(bag)) return secrets.get(bag) ?? bag;
13096
13277
  return source;
13097
13278
  }
13098
- return positionByEmbeddedSpan(bag, source, secrets);
13279
+ return positionByEmbeddedSpan(bag, source, secrets, bagIsSameGeneration);
13099
13280
  }
13100
13281
  if (typeof bag === "string" && isPlainObject$2(source)) {
13101
13282
  const certified = positionByCrossStackSource(bag, source, secrets);
@@ -13124,16 +13305,16 @@ function redactByPath(bag, source, secrets, rules, secretExpressions) {
13124
13305
  const positionalIsExact = rules.descendArrays && bag.length === source.length && orderPreserved;
13125
13306
  return bag.map((item, i) => {
13126
13307
  const j = partnerIndex[i];
13127
- if (j >= 0) return redactByPath(item, source[j], secrets, rules, secretExpressions);
13128
- if (positionalIsExact) return redactByPath(item, source[i], secrets, rules, secretExpressions);
13308
+ if (j >= 0) return redactByPath(item, source[j], secrets, rules, secretExpressions, bagIsSameGeneration);
13309
+ if (positionalIsExact) return redactByPath(item, source[i], secrets, rules, secretExpressions, bagIsSameGeneration);
13129
13310
  return redactSecretsForState(item, secrets);
13130
13311
  });
13131
13312
  }
13132
- if (rules.descendArrays && bag.length === source.length) return bag.map((item, i) => redactByPath(item, source[i], secrets, rules, secretExpressions));
13313
+ if (rules.descendArrays && bag.length === source.length) return bag.map((item, i) => redactByPath(item, source[i], secrets, rules, secretExpressions, bagIsSameGeneration));
13133
13314
  }
13134
13315
  if (isPlainObject$2(bag) && isPlainObject$2(source)) {
13135
13316
  const out = Object.create(null);
13136
- for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? redactByPath(v, source[k], secrets, rules, secretExpressions) : redactSecretsForState(v, secrets);
13317
+ for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? redactByPath(v, source[k], secrets, rules, secretExpressions, bagIsSameGeneration) : redactSecretsForState(v, secrets);
13137
13318
  return out;
13138
13319
  }
13139
13320
  return redactSecretsForState(bag, secrets);
@@ -14151,7 +14332,7 @@ function refuseUncertifiedReadbackPositions(bag, source, secrets, learn, mark) {
14151
14332
  function redactSecretsForState(bag, secrets, source, rules = TEMPLATE_DERIVED_RULES) {
14152
14333
  if (secrets.size === 0 && source === void 0) return bag;
14153
14334
  if (source !== void 0) {
14154
- const positioned = redactByPath(bag, source, secrets, rules, recordedExpressionsOf(secrets));
14335
+ const positioned = redactByPath(bag, source, secrets, rules, recordedExpressionsOf(secrets), isSameGenerationBag(bag));
14155
14336
  if (!isReadbackProjectedFromState(rules)) return positioned;
14156
14337
  const refused = refuseUncertifiedReadbackPositions(positioned, source, secrets);
14157
14338
  const derived = deriveReadbackNeedles(bag, source, secrets, rules);
@@ -14991,7 +15172,10 @@ var DagBuilder = class {
14991
15172
  this.logger.debug(`Dependency graph built: ${resourceIds.length} nodes, ${edgeCount} edges`);
14992
15173
  edgeCount += this.addCustomResourcePolicyEdges(graph, template);
14993
15174
  edgeCount += this.addLambdaVpcEdges(graph, template);
14994
- if (!alg.isAcyclic(graph)) throw new DependencyError(`Circular dependency detected in template. Cycles: ${this.findCycles(graph).map((c) => c.join(" -> ")).join("; ")}`);
15175
+ if (!alg.isAcyclic(graph)) {
15176
+ const cycles = this.findCycles(graph);
15177
+ throw new DependencyError(`Circular dependency detected in template. Cycles: ${cycles.map((c) => c.join(" -> ")).join("; ")}`);
15178
+ }
14995
15179
  return graph;
14996
15180
  }
14997
15181
  /**
@@ -15020,7 +15204,10 @@ var DagBuilder = class {
15020
15204
  const predecessors = graphCopy.predecessors(node);
15021
15205
  return !predecessors || predecessors.length === 0;
15022
15206
  });
15023
- if (readyNodes.length === 0) throw new DependencyError(`Circular dependency detected. Remaining nodes: ${graphCopy.nodes().join(", ")}`);
15207
+ if (readyNodes.length === 0) {
15208
+ const remaining = graphCopy.nodes();
15209
+ throw new DependencyError(`Circular dependency detected. Remaining nodes: ${remaining.join(", ")}`);
15210
+ }
15024
15211
  this.logger.debug(`Level ${levelNum}: ${readyNodes.length} resources - ${readyNodes.join(", ")}`);
15025
15212
  levels.push(readyNodes);
15026
15213
  readyNodes.forEach((node) => {
@@ -15826,9 +16013,10 @@ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15826
16013
  * 4s/8s steps overshoot it. The dense schedule applies ONLY when the caller
15827
16014
  * left the schedule at its defaults — a caller that passed its own
15828
16015
  * `maxRetries` / `initialDelayMs` / `maxDelayMs` / `isRetryable` picked that
15829
- * schedule deliberately (e.g. the DELETE path's 3 x 5s, or the delete-then-
15830
- * re-create sites' ~64s budget covering SQS's 60s name cooldown) and gets it
15831
- * verbatim.
16016
+ * schedule deliberately (e.g. the DELETE path's 3 retries from 5s 5s/8s/8s,
16017
+ * since it passes `initialDelayMs` but leaves `maxDelayMs` at the 8s default
16018
+ * or the delete-then-re-create sites' ~64s budget covering SQS's 60s name
16019
+ * cooldown) and gets it verbatim.
15832
16020
  *
15833
16021
  * A THIRD class rides its own grid on the default schedule since issue
15834
16022
  * [#2116](https://github.com/go-to-k/cdkd/issues/2116): a NAME COOLDOWN (a
@@ -16511,6 +16699,7 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
16511
16699
  "RoleArn"
16512
16700
  ]),
16513
16701
  silentDrop: /* @__PURE__ */ new Map([
16702
+ ["CapacityProviderConfiguration", "not yet implemented by cdkd"],
16514
16703
  ["FilesystemConfigurations", "not yet implemented by cdkd"],
16515
16704
  ["RequestHeaderConfiguration", "not yet implemented by cdkd"],
16516
16705
  ["Tags", "not yet implemented by cdkd"]
@@ -16629,7 +16818,8 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
16629
16818
  silentDrop: /* @__PURE__ */ new Map([
16630
16819
  ["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."],
16631
16820
  ["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."],
16632
- ["EvaluationWindow", "not yet implemented by cdkd"]
16821
+ ["EvaluationWindow", "not yet implemented by cdkd"],
16822
+ ["WarmUpConfiguration", "not yet implemented by cdkd"]
16633
16823
  ]),
16634
16824
  createOnlyDrops: /* @__PURE__ */ new Set()
16635
16825
  }],
@@ -17457,7 +17647,7 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
17457
17647
  }],
17458
17648
  ["AWS::Glue::Connection", {
17459
17649
  handled: /* @__PURE__ */ new Set(["CatalogId", "ConnectionInput"]),
17460
- silentDrop: /* @__PURE__ */ new Map(),
17650
+ silentDrop: /* @__PURE__ */ new Map([["Tags", "not yet implemented by cdkd"]]),
17461
17651
  createOnlyDrops: /* @__PURE__ */ new Set()
17462
17652
  }],
17463
17653
  ["AWS::Glue::Crawler", {
@@ -18021,7 +18211,6 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
18021
18211
  ["UseLatestRestorableTime", "not yet implemented by cdkd"]
18022
18212
  ]),
18023
18213
  createOnlyDrops: /* @__PURE__ */ new Set([
18024
- "AvailabilityZones",
18025
18214
  "ClusterScalabilityType",
18026
18215
  "DBSystemId",
18027
18216
  "EngineMode",
@@ -19370,9 +19559,7 @@ var DiffCalculator = class DiffCalculator {
19370
19559
  case "DELETE":
19371
19560
  summary.delete++;
19372
19561
  break;
19373
- case "NO_CHANGE":
19374
- summary.noChange++;
19375
- break;
19562
+ case "NO_CHANGE": summary.noChange++;
19376
19563
  }
19377
19564
  return summary;
19378
19565
  }
@@ -21122,7 +21309,8 @@ var WAFv2WebACLProvider = class {
21122
21309
  this.logger.debug(`Successfully deleted WAFv2 WebACL ${logicalId}`);
21123
21310
  } catch (error) {
21124
21311
  if (error instanceof WAFNonexistentItemException) {
21125
- assertRegionMatch(await this.getClient().config.region(), context?.expectedRegion, resourceType, logicalId, physicalId);
21312
+ const clientRegion = await this.getClient().config.region();
21313
+ assertRegionMatch(clientRegion, context?.expectedRegion, resourceType, logicalId, physicalId);
21126
21314
  this.logger.debug(`WAFv2 WebACL ${physicalId} does not exist, skipping deletion`);
21127
21315
  return;
21128
21316
  }
@@ -21224,7 +21412,8 @@ var WAFv2WebACLProvider = class {
21224
21412
  result["TokenDomains"] = webACL.TokenDomains ? [...webACL.TokenDomains] : [];
21225
21413
  if (webACL.AssociationConfig) result["AssociationConfig"] = webACL.AssociationConfig;
21226
21414
  try {
21227
- result["Tags"] = normalizeAwsTagsToCfn((await this.getClient().send(new ListTagsForResourceCommand$11({ ResourceARN: physicalId }))).TagInfoForResource?.TagList);
21415
+ const tagsResp = await this.getClient().send(new ListTagsForResourceCommand$11({ ResourceARN: physicalId }));
21416
+ result["Tags"] = normalizeAwsTagsToCfn(tagsResp.TagInfoForResource?.TagList);
21228
21417
  } catch (err) {
21229
21418
  this.logger.debug(`WAFv2 ListTagsForResource(${physicalId}) failed: ${err instanceof Error ? err.message : String(err)}`);
21230
21419
  }
@@ -22929,14 +23118,15 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
22929
23118
  const resolved = {};
22930
23119
  for (const [key, val] of Object.entries(obj)) {
22931
23120
  const resolvedVal = await this.resolveValue(val, context);
22932
- if (resolvedVal !== AWS_NO_VALUE) if (key === "__proto__") Object.defineProperty(resolved, key, {
22933
- value: resolvedVal,
22934
- enumerable: true,
22935
- writable: true,
22936
- configurable: true
22937
- });
22938
- else resolved[key] = resolvedVal;
22939
- else this.logger.debug(`Property ${key} resolved to AWS::NoValue, omitting from object`);
23121
+ if (resolvedVal !== AWS_NO_VALUE) {
23122
+ if (key === "__proto__") Object.defineProperty(resolved, key, {
23123
+ value: resolvedVal,
23124
+ enumerable: true,
23125
+ writable: true,
23126
+ configurable: true
23127
+ });
23128
+ else resolved[key] = resolvedVal;
23129
+ } else this.logger.debug(`Property ${key} resolved to AWS::NoValue, omitting from object`);
22940
23130
  }
22941
23131
  return resolved;
22942
23132
  }
@@ -23516,6 +23706,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
23516
23706
  case "ApiId": return physicalId;
23517
23707
  default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
23518
23708
  }
23709
+ if (resourceType === "AWS::ApiGatewayV2::Api") switch (attributeName) {
23710
+ case "ExecuteApiArn": return `arn:${partition}:execute-api:${region}:${accountId}:${physicalId}`;
23711
+ case "ApiId": return physicalId;
23712
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
23713
+ }
23519
23714
  if (resourceType === "AWS::ServiceDiscovery::PrivateDnsNamespace" || resourceType === "AWS::ServiceDiscovery::HttpNamespace" || resourceType === "AWS::ServiceDiscovery::PublicDnsNamespace") switch (attributeName) {
23520
23715
  case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:namespace/${physicalId}`;
23521
23716
  case "Id": return physicalId;
@@ -23649,9 +23844,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
23649
23844
  case "PublicDnsName":
23650
23845
  value = instance?.PublicDnsName;
23651
23846
  break;
23652
- case "AvailabilityZone":
23653
- value = instance?.Placement?.AvailabilityZone;
23654
- break;
23847
+ case "AvailabilityZone": value = instance?.Placement?.AvailabilityZone;
23655
23848
  }
23656
23849
  if (value !== void 0 && value !== null && value !== "") {
23657
23850
  cachedEc2InstanceAttributes[cacheKey] = value;
@@ -24775,7 +24968,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
24775
24968
  const credentials = await assumeRoleForCrossAccountStateRead(roleArn);
24776
24969
  const { bucket, region: bucketRegion } = await resolveCrossAccountStateBucket(parsed.accountId, credentials);
24777
24970
  const prefix = context.stateBackend?.prefix ?? "cdkd";
24778
- return new S3StateBackend(new S3Client({
24971
+ const s3 = new S3Client({
24779
24972
  ...awsClientDefaults(),
24780
24973
  region: bucketRegion,
24781
24974
  credentials: {
@@ -24789,7 +24982,8 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
24789
24982
  warn: () => {},
24790
24983
  error: () => {}
24791
24984
  }
24792
- }), {
24985
+ });
24986
+ return new S3StateBackend(s3, {
24793
24987
  bucket,
24794
24988
  prefix
24795
24989
  }, {
@@ -25984,7 +26178,7 @@ function unsupportedTypeIssueUrl(resourceType) {
25984
26178
 
25985
26179
  //#endregion
25986
26180
  //#region src/provisioning/slow-cc-operation-timeouts.ts
25987
- const MINUTE_MS = 60 * 1e3;
26181
+ const MINUTE_MS = 6e4;
25988
26182
  /**
25989
26183
  * 60 min covers the worst-case observed for each type with headroom over the
25990
26184
  * 15-30 min typical range. Keyed by CloudFormation type name. RDS / ElastiCache
@@ -26307,7 +26501,7 @@ var CloudControlProvider = class {
26307
26501
  cloudControlClient;
26308
26502
  logger = getLogger().child("CloudControlProvider");
26309
26503
  patchGenerator = new JsonPatchGenerator();
26310
- MAX_WAIT_TIME_MS = 900 * 1e3;
26504
+ MAX_WAIT_TIME_MS = 9e5;
26311
26505
  INITIAL_POLL_INTERVAL_MS = 1e3;
26312
26506
  MAX_POLL_INTERVAL_MS = 1e4;
26313
26507
  constructor() {
@@ -26466,8 +26660,9 @@ var CloudControlProvider = class {
26466
26660
  const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
26467
26661
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
26468
26662
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
26469
- const { ASGProvider } = await import("./asg-provider-4QzW_6q5.js").then((n) => n.n);
26470
- return withIndeterminateGuard(await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
26663
+ const { ASGProvider } = await import("./asg-provider-CQrHElrl.js").then((n) => n.n);
26664
+ const asgProvider = new ASGProvider();
26665
+ return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
26471
26666
  }
26472
26667
  const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
26473
26668
  if (isProtectedEc2Instance) await disableInstanceApiTermination(getAwsClients().ec2, physicalId, this.logger);
@@ -27119,16 +27314,13 @@ var CloudControlProvider = class {
27119
27314
  }
27120
27315
  }
27121
27316
  break;
27122
- case "AWS::ResourceGroups::Group":
27123
- if (!enriched["Arn"]) {
27124
- const model = await this.readCcResourceModel(resourceType, physicalId);
27125
- if (model) {
27126
- if (typeof model["Arn"] === "string") enriched["Arn"] = model["Arn"];
27127
- this.logger.debug(`Enriched ResourceGroups Group ${physicalId} with Arn from CC GetResource`);
27128
- }
27317
+ case "AWS::ResourceGroups::Group": if (!enriched["Arn"]) {
27318
+ const model = await this.readCcResourceModel(resourceType, physicalId);
27319
+ if (model) {
27320
+ if (typeof model["Arn"] === "string") enriched["Arn"] = model["Arn"];
27321
+ this.logger.debug(`Enriched ResourceGroups Group ${physicalId} with Arn from CC GetResource`);
27129
27322
  }
27130
- break;
27131
- default: break;
27323
+ }
27132
27324
  }
27133
27325
  return enriched;
27134
27326
  }
@@ -30321,7 +30513,8 @@ var IAMRoleProvider = class {
30321
30513
  await this.iamClient.send(new GetRoleCommand({ RoleName: physicalId }));
30322
30514
  } catch (error) {
30323
30515
  if (error instanceof NoSuchEntityException) {
30324
- assertRegionMatch(await this.iamClient.config.region(), context?.expectedRegion, resourceType, logicalId, physicalId);
30516
+ const clientRegion = await this.iamClient.config.region();
30517
+ assertRegionMatch(clientRegion, context?.expectedRegion, resourceType, logicalId, physicalId);
30325
30518
  this.logger.debug(`Role ${physicalId} does not exist, skipping deletion`);
30326
30519
  return;
30327
30520
  }
@@ -31065,6 +31258,67 @@ function secretBearingExportNameWarning(outputKey, exportName, exposure) {
31065
31258
  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.`;
31066
31259
  }
31067
31260
  /**
31261
+ * Test `key` for recorded secret plaintext and return how it may be shown.
31262
+ *
31263
+ * Reuses {@link stateKeySecretExposure} and the same `maskEveryOccurrence` the
31264
+ * warnings below use, rather than restating either: two spellings of "is this
31265
+ * key safe to print" would disagree on the boundary cases those two encode
31266
+ * (the whole-key match for a sub-floor needle, longest-needle-first masking).
31267
+ *
31268
+ * SANITISED WITH BOTH helpers, because neither is a superset of the other —
31269
+ * measured, after a first cut swapped one for the other and silently traded
31270
+ * one class of character for another (issue #2667 review):
31271
+ *
31272
+ * | input | `stripControlChars` | `displaySafe` |
31273
+ * | -------- | ------------------- | ------------- |
31274
+ * | `U+200E` | removed | KEPT |
31275
+ * | `U+200F` | removed | KEPT |
31276
+ * | `U+2028` | KEPT | replaced |
31277
+ * | `U+2029` | KEPT | replaced |
31278
+ *
31279
+ * `U+2028` / `U+2029` matter because this text is PERSISTED and re-rendered by
31280
+ * JSON and web log viewers that treat both as line terminators — the CI-log
31281
+ * surface this masking exists to protect, where an `Fn::Sub`-built export name
31282
+ * carrying one could forge a log line (`display-safe.ts` states that
31283
+ * rationale). `U+200E` / `U+200F` are the bidi MARKS, named as residuals in
31284
+ * that same file; they reorder rendered text without terminating a line. On a
31285
+ * path whose subject is a possibly-secret-bearing name in an operator's log,
31286
+ * neither loss is worth taking, and composing costs nothing.
31287
+ *
31288
+ * ORDER IS LOAD-BEARING, and for the OVERLAP set — not for the marks, which
31289
+ * an earlier revision of this comment named and measurement contradicted.
31290
+ * `displaySafe` never touches `U+200E` / `U+200F`, so those give identical
31291
+ * output either way. Where the two classes OVERLAP — `U+0000`-`U+001F`,
31292
+ * `U+007F`-`U+009F`, `U+202A`-`U+202E`, `U+2066`-`U+2069` — `stripControlChars`
31293
+ * DELETES while `displaySafe` replaces with a space, so strip-then-display
31294
+ * yields `"ab"` and display-then-strip yields `"a b"`. Stripping first keeps a
31295
+ * name carrying them from being padded out.
31296
+ *
31297
+ * `displaySafe` also `.trim()`s, which `stripControlChars` alone did not: a
31298
+ * display-shape change for a key with leading or trailing whitespace. Stated
31299
+ * because it is a real difference, not hidden.
31300
+ *
31301
+ * The sibling warnings in this file still use `stripControlChars` ALONE and
31302
+ * carry the `U+2028` half of the gap; widening that helper, or converting
31303
+ * them, changes call sites this issue does not touch, so it is filed as issue
31304
+ * [#2874](https://github.com/go-to-k/cdkd/issues/2874) rather than done here.
31305
+ */
31306
+ function secretSafeKeyDisplay(key, secrets) {
31307
+ const sanitise = (text) => displaySafe(stripControlChars(text));
31308
+ const shown = sanitise(key);
31309
+ const exposure = stateKeySecretExposure(shown, secrets) ?? stateKeySecretExposure(key, secrets);
31310
+ if (!exposure) return {
31311
+ kind: "safe",
31312
+ text: shown
31313
+ };
31314
+ const masked = sanitise(maskEveryOccurrence(shown, exposure));
31315
+ if (masked === shown) return { kind: "withheld" };
31316
+ return {
31317
+ kind: "masked",
31318
+ text: masked
31319
+ };
31320
+ }
31321
+ /**
31068
31322
  * Warning for a state KEY that already holds secret plaintext — the residue an
31069
31323
  * EARLIER binary left when it published an export name that resolved to one.
31070
31324
  *
@@ -32121,9 +32375,9 @@ const EBS_FINAL_SNAPSHOT_TAG_KEY = "cdkd:final-snapshot-of";
32121
32375
  /** Test seam (matches the `describe-type.ts` / macro-expander pattern). */
32122
32376
  const finalSnapshotDelays = { sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) };
32123
32377
  const PRE_DELETE_SNAPSHOT_POLL_INTERVAL_MS = 5e3;
32124
- const PRE_DELETE_SNAPSHOT_TIMEOUT_MS = 3600 * 1e3;
32378
+ const PRE_DELETE_SNAPSHOT_TIMEOUT_MS = 36e5;
32125
32379
  /** Post-snapshot settle budget for a Redshift cluster before its delete. */
32126
- const REDSHIFT_CLUSTER_SETTLE_TIMEOUT_MS = 1200 * 1e3;
32380
+ const REDSHIFT_CLUSTER_SETTLE_TIMEOUT_MS = 12e5;
32127
32381
  function errMsg(error) {
32128
32382
  return error instanceof Error ? error.message : String(error);
32129
32383
  }
@@ -32507,6 +32761,397 @@ async function createReplicationGroupFinalSnapshot(client, replicationGroupId, l
32507
32761
  });
32508
32762
  }
32509
32763
 
32764
+ //#endregion
32765
+ //#region src/analyzer/skipped-outputs.ts
32766
+ /**
32767
+ * The record `cdkd deploy` leaves behind for an Output it could NOT resolve
32768
+ * and SKIPPED, and the digest `cdkd diff` compares it against (issue
32769
+ * [#2740](https://github.com/go-to-k/cdkd/issues/2740)).
32770
+ *
32771
+ * The deploy side (`DeployEngine.handleOutputResolutionFailure`, default arm)
32772
+ * warns, stores `undefined` for the key and moves on, so a re-resolved
32773
+ * `state.outputs` lacks it (the no-change path keeps the previous bag whole
32774
+ * when any output fails, so a key that resolved on an EARLIER deploy can keep
32775
+ * its value beside a record — the reader checks absence first and ignores the
32776
+ * record then). The diff side resolves outputs with `skipDynamicReferences`, so a
32777
+ * failure that happens INSIDE a secret lookup — a JSON key the secret does not
32778
+ * hold, a reference assembled from another secret's value — does not reproduce
32779
+ * there: the value assembles into its token, the key is absent from state, and
32780
+ * `computeOutputsDiff` pushed an `ADD` the deploy would never perform, on every
32781
+ * run of an unchanged stack. The diff cannot tell from assembly alone that the
32782
+ * deploy would fail (a composed string carrying a secret reference is a
32783
+ * legitimate output), so it learns what the deploy learned instead:
32784
+ * `StackState.skippedOutputs` maps each skipped key to a digest of the
32785
+ * template inputs its resolution read, and the diff trusts the record only
32786
+ * while that digest is unchanged.
32787
+ *
32788
+ * ## What the digest covers, and why
32789
+ *
32790
+ * `skippedOutputDigest` hashes the output's OWN entry (`Value`, `Export`,
32791
+ * `Condition`, whatever else it declares) together with every top-level
32792
+ * template section EXCEPT `Resources` and `Outputs`:
32793
+ *
32794
+ * - The entry itself: repairing the `Value` expression is the obvious repair.
32795
+ * The `Export.Name` is in it too, because the alias pass routes an
32796
+ * `Export.Name` failure through the same handler, which blanks the output's
32797
+ * own key even when its value resolved — so a repaired or removed export
32798
+ * name must invalidate the record as well.
32799
+ * - `Parameters` (a `Ref`-built reference repaired through a parameter
32800
+ * default), `Conditions` (an `Fn::If` whose branch flips), `Mappings` (an
32801
+ * `Fn::FindInMap`-built key), and the remaining sections for the same
32802
+ * reason, spelled as one rule rather than a list that drifts: everything an
32803
+ * output's resolution can read besides resources. One VALUE inside them is
32804
+ * excluded — the `Default` of a `NoEcho: true` parameter, hashed as a
32805
+ * constant; see `withNoEchoDefaultsMasked` for why. That is the POSITION,
32806
+ * not the parameter: its `AllowedValues`, and any literal an author puts in
32807
+ * `Conditions`, `Rules` or `Metadata`, are hashed as before.
32808
+ * - NOT `Resources`: hashing the section would invalidate the record on every
32809
+ * unrelated resource edit. The output CAN be repaired from that side —
32810
+ * `collectSkippedOutputs` records both skip arms, and the quiet one is an
32811
+ * `Fn::GetAtt` whose attribute `constructAttribute` could not build — so a
32812
+ * digest alone would keep binding while the next deploy publishes the row
32813
+ * and its `Export.Name`, the phantom's inverse. {@link
32814
+ * bindingSkippedOutputs} closes that with the CHANGE MAP rather than the
32815
+ * digest: a key whose entry references a logical id with a pending resource
32816
+ * change does not bind. The diff has that map (it ran the resource diff
32817
+ * first); the deploy does not need it, because a stack with any resource
32818
+ * change takes the path that re-resolves every output and rewrites the
32819
+ * record. REFERENCE, not repair: whether an edit could actually make the
32820
+ * output resolvable is undecidable from a template, so an unrelated edit to
32821
+ * a referenced resource un-binds too and the key can preview as an `ADD`
32822
+ * again. The cost is bounded — a run with a changed resource is already
32823
+ * reporting that row, so `--fail` exits 1 regardless — while the case it
32824
+ * buys depends on whether the diff can resolve the output at all. For an
32825
+ * output reading an attribute that does not exist yet — the SECOND skip arm
32826
+ * this record covers, not the secret lookup issue #2740 was filed on — it is
32827
+ * the VERDICT and not the row: unresolvable in
32828
+ * both readings, so bound the diff reports the outputs settled while the
32829
+ * next deploy is about to publish that key and its `Export.Name`. For an
32830
+ * output the diff CAN resolve, whose reference is to a name rather than to
32831
+ * a pending attribute (an `Fn::Sub` over an SSM parameter's name, say), the
32832
+ * row IS the difference and reappears as an `ADD` the moment the record
32833
+ * stops binding.
32834
+ * - NOT the sibling `Outputs`: a CloudFormation output cannot reference
32835
+ * another output, so a sibling change cannot repair this one, and excluding
32836
+ * them keeps an unrelated output edit from producing a one-run phantom.
32837
+ *
32838
+ * What the digest CANNOT see is a repair outside the template — the secret
32839
+ * gained the JSON key, the SSM parameter was created, a parameter VALUE
32840
+ * handed to the engine rather than declared changed (a nested stack's inputs
32841
+ * from its parent, `DeployEngineOptions.parameters`; `Parameters`
32842
+ * DECLARATIONS are hashed, supplied values are not), or **cdkd itself was
32843
+ * upgraded**: the quiet skip arm is `constructAttribute` returning nothing,
32844
+ * so a provider that gains that attribute repairs the output while neither
32845
+ * the template nor any resource moves.
32846
+ *
32847
+ * They are blind spots for three different reasons, and only the first is
32848
+ * about knowledge. The secret's key set and the SSM parameter's existence
32849
+ * need the external lookup the diff deliberately refuses to make. A supplied
32850
+ * parameter VALUE is not one of those — a nested stack's inputs are resolved
32851
+ * and forwarded to the child diff (`resolveChildStackParameters`) — it is
32852
+ * simply outside what the digest hashes, because hashing values would make
32853
+ * the record depend on a caller's arguments rather than on the template. And
32854
+ * the writing binary's version is knowable and merely unstored: keeping it
32855
+ * beside the digest and un-binding on mismatch would close that one. All four
32856
+ * end the same way: the record keeps binding until the next deploy re-resolves
32857
+ * the output, and that deploy publishes the key — unless a SIBLING output is
32858
+ * still unresolved on a run with no resource change, where the engine keeps
32859
+ * the previous outputs bag wholesale and the key stays unpublished for a
32860
+ * further run (go-to-k/cdkd#2771, pre-existing and not introduced here).
32861
+ * Documented as the accepted limitation in `docs/cli-diff.md`; it is narrower
32862
+ * than the pre-#2740 behaviour, where the diff was wrong on EVERY run.
32863
+ *
32864
+ * A repair on the RESOURCE side is NOT on that list either, but the reason is
32865
+ * narrower than it first looks. The change map above closes it only when the
32866
+ * repair arrives WITH a template resource change, which is what makes the
32867
+ * resource show on the diff at all. An OUT-OF-BAND state write does not:
32868
+ * every writer that rebuilds state outside a deploy DROPS the record rather
32869
+ * than carry it (one enumerated exception, the partial-destroy snapshot, is
32870
+ * explained with the field) — `cdkd import` refreshes `attributes` for the
32871
+ * resources it imports, `cdkd drift --accept` rewrites the properties an
32872
+ * attribute may be built from, `cdkd rollback`'s replacement arm rebuilds a
32873
+ * record with the attributes a FRESH create returned, and the rest are listed
32874
+ * with the field.
32875
+ * Dropping returns those keys to pre-#2740 behaviour until the next deploy
32876
+ * recomputes the record — a row where the diff can resolve them, the ordinary
32877
+ * whole-section suppression where it cannot — which is what keeps a
32878
+ * resource-side repair out of the list above.
32879
+ *
32880
+ * The rule is flat and it is meant to be: EVERY writer that rebuilds state
32881
+ * outside a deploy drops the record, the partial-destroy snapshot being the
32882
+ * one enumerated exception — the full list lives with the field, in
32883
+ * `StackState.skippedOutputs`. Three per-writer arguments for carrying it were
32884
+ * written during review, which is the reason for the flat rule rather than an
32885
+ * aside: two were shown wrong (substituting a value repairs an output whose
32886
+ * enclosing intrinsic was choking on what was there; scrubbing a plaintext
32887
+ * back to its expression rewrites a string outputs read verbatim) and the
32888
+ * third — deleting a property EXPOSING an attribute of the same name — could
32889
+ * not be settled either way, which argues for flatness at least as strongly.
32890
+ *
32891
+ * ## Where the digest must be taken from
32892
+ *
32893
+ * The template AS HANDED to the engine / to `computeStackDiff` — before
32894
+ * parameter binding, condition evaluation or any output resolution. The
32895
+ * INVARIANT, and the reason both callers `structuredClone` rather than pass
32896
+ * their live object: **no resolution may be visible to the digest, on either
32897
+ * side, and both sides must take it at the same point of their own flow.**
32898
+ * Two independent reasons, neither of which depends on any particular
32899
+ * resolver doing the writing: a digest that could see a resolved value would
32900
+ * (1) differ between the two sides, which computes a record the diff can
32901
+ * never re-derive, and (2) fingerprint a secret into `state.json`, since a
32902
+ * resolved value can be a decrypted one.
32903
+ *
32904
+ * The clone is therefore NOT dead weight even while no resolver mutates its
32905
+ * input. It used to be load-bearing against a live rewrite — `resolveSub`
32906
+ * substituted into the caller's two-argument `Fn::Sub` variable map until
32907
+ * go-to-k/cdkd#2764 gave it a fresh object — and the protection was never
32908
+ * derived from that call site: it is derived from the invariant above, which
32909
+ * a future in-place optimisation anywhere in either flow would silently
32910
+ * violate. `tests/unit/cli/diff-recursive-skipped-outputs.test.ts` pins the
32911
+ * current no-mutation contract from the other direction (the template handed
32912
+ * to `computeStackDiff` comes back byte-identical), so reintroducing a
32913
+ * rewrite reds a test rather than moving a digest;
32914
+ * `tests/unit/deployment/deploy-engine-skipped-outputs.test.ts` INJECTS a
32915
+ * rewrite through its resolver mock and requires the recorded digest to equal
32916
+ * a fresh parse's, which pins the ordering itself.
32917
+ *
32918
+ * Given that contract the hash covers template text only. It is over
32919
+ * canonical JSON (object keys sorted at every level), so a template
32920
+ * re-serialised with keys in another order digests identically.
32921
+ *
32922
+ * ## What the diff does with a binding record
32923
+ *
32924
+ * It previews the key as ABSENT — no row — because that is exactly what the
32925
+ * deploy will leave in state. It does NOT flag the section as failed: a
32926
+ * sibling output that genuinely changed still renders, and `--fail` still
32927
+ * exits 1 for it. A record whose digest no longer matches is simply ignored:
32928
+ * the output is previewed under the ordinary rules again (usually an `ADD`;
32929
+ * an intrinsic `Export.Name` the diff still cannot resolve keeps omitting the
32930
+ * section, as before), the next deploy re-decides it —
32931
+ * publishing it if the repair took, or recording it again under the new
32932
+ * digest — and the diff follows that decision.
32933
+ */
32934
+ /**
32935
+ * JSON with object keys sorted at every level. Arrays keep their order (an
32936
+ * `Fn::Join` list is positional). `undefined` members are dropped, as
32937
+ * `JSON.stringify` drops them, so a parsed template and its in-memory twin
32938
+ * digest identically.
32939
+ */
32940
+ function canonicalJson(value) {
32941
+ return JSON.stringify(value, (_key, v) => {
32942
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) {
32943
+ const sorted = Object.create(null);
32944
+ for (const k of Object.keys(v).sort()) sorted[k] = v[k];
32945
+ return sorted;
32946
+ }
32947
+ return v;
32948
+ });
32949
+ }
32950
+ /** The constant a `NoEcho` `Default` is hashed as. Its text is irrelevant; that
32951
+ * it is CONSTANT is the point. */
32952
+ const NO_ECHO_DEFAULT_MASK = "<cdkd:noecho-default>";
32953
+ /**
32954
+ * `Parameters` with every `NoEcho: true` parameter's `Default` replaced by a
32955
+ * constant, for hashing only.
32956
+ *
32957
+ * The pre-resolution snapshot already keeps RESOLVED secret values out of the
32958
+ * digest. An authored `Default` under `NoEcho: true` is not one of those — it
32959
+ * is template text, and it went into the hash. With the rest of the
32960
+ * non-`Resources` sections known, a reader of `state.json` holds a confirm
32961
+ * ORACLE for a low-entropy default: guess it, recompute, compare.
32962
+ *
32963
+ * This does NOT claim the value is otherwise absent from state — a parameter a
32964
+ * resource reads can persist its RESOLVED default in that resource's
32965
+ * properties, and parameter `NoEcho` is outside the state-redaction model
32966
+ * either way. What it claims is narrower and still worth having: this record
32967
+ * must not ADD an oracle of its own for the case where the value is not
32968
+ * already there.
32969
+ *
32970
+ * The cost, stated rather than hidden: changing ONLY such a default no longer
32971
+ * moves the digest, so the record keeps binding through that edit until the
32972
+ * next deploy re-resolves the output. That is the same bounded staleness the
32973
+ * documented blind spots carry.
32974
+ *
32975
+ * Everything else about the parameter is still hashed — its `Type`, its
32976
+ * `AllowedValues`, the `NoEcho` flag itself — so a parameter appearing,
32977
+ * disappearing or changing shape still un-binds. Unmatched parameter content
32978
+ * hashes exactly as before: the map is rebuilt, but the canonicaliser sorts
32979
+ * keys and ignores prototypes, so a template with no `NoEcho` default gets the
32980
+ * digest it always had.
32981
+ */
32982
+ function withNoEchoDefaultsMasked(parameters) {
32983
+ if (parameters === null || typeof parameters !== "object" || Array.isArray(parameters)) return parameters;
32984
+ const masked = Object.create(null);
32985
+ for (const [name, decl] of Object.entries(parameters)) {
32986
+ const body = decl;
32987
+ masked[name] = typeof body === "object" && body !== null && body["NoEcho"] === true && "Default" in body ? {
32988
+ ...body,
32989
+ Default: NO_ECHO_DEFAULT_MASK
32990
+ } : decl;
32991
+ }
32992
+ return masked;
32993
+ }
32994
+ /**
32995
+ * The digest recorded in `StackState.skippedOutputs[outputKey]` by the deploy
32996
+ * and recomputed by the diff — see the module doc for what it covers. Both
32997
+ * sides call THIS function, so the coverage is spelled once.
32998
+ *
32999
+ * `template` must be the pre-resolution snapshot the module doc describes.
33000
+ */
33001
+ function skippedOutputDigest(template, outputKey) {
33002
+ const sections = Object.create(null);
33003
+ for (const [section, body] of Object.entries(template)) {
33004
+ if (section === "Resources" || section === "Outputs") continue;
33005
+ sections[section] = section === "Parameters" ? withNoEchoDefaultsMasked(body) : body;
33006
+ }
33007
+ const entry = template.Outputs?.[outputKey];
33008
+ return createHash("sha256").update(canonicalJson({
33009
+ output: entry,
33010
+ template: sections
33011
+ })).digest("hex");
33012
+ }
33013
+ /**
33014
+ * The record a deploy writes for the bag `resolveOutputs` produced: every key
33015
+ * whose value is `undefined`, mapped to its digest. Two arms write that value:
33016
+ * the default arm of `handleOutputResolutionFailure` (the resolver THREW —
33017
+ * warned, and refused under `--strict-getatt`), and a resolver that returned
33018
+ * `undefined` outright without throwing (an attribute it could construct
33019
+ * nothing for, `constructAttribute`'s empty arm — no warn, no strict refusal).
33020
+ * Both leave the key out of a re-resolved bag (the no-change path may keep a
33021
+ * previous value under it, see the module doc), and that path's
33022
+ * `resolutionFailed` treats both the same, so this record does too: the
33023
+ * question it answers is "will the next deploy leave this key absent?", and
33024
+ * the answer is yes either way. `undefined` (the field OMITTED) when nothing
33025
+ * was skipped, so a record that carries the field says at least one output
33026
+ * was skipped.
33027
+ *
33028
+ * `template` must be the pre-resolution snapshot (see the module doc).
33029
+ */
33030
+ function collectSkippedOutputs(template, resolvedOutputs) {
33031
+ let record;
33032
+ for (const [outputKey, value] of Object.entries(resolvedOutputs)) {
33033
+ if (value !== void 0) continue;
33034
+ record ??= Object.create(null);
33035
+ record[outputKey] = skippedOutputDigest(template, outputKey);
33036
+ }
33037
+ return record;
33038
+ }
33039
+ /**
33040
+ * Every resource logical id an output entry could reference, from `Ref`,
33041
+ * either `Fn::GetAtt` spelling, and an `Fn::Sub` placeholder, walked to any
33042
+ * depth over the whole entry (`Value`, `Export.Name` and anything else it
33043
+ * declares).
33044
+ *
33045
+ * A deliberate SUPERSET: an `Fn::Sub` placeholder naming a template
33046
+ * PARAMETER, or a `Ref` to one, lands here too, since telling the two apart
33047
+ * needs the `Parameters` section and the answer only ever widens the set. The
33048
+ * one consumer intersects it with the ids that actually have a pending
33049
+ * RESOURCE change, so a name that is not a resource cannot match, and a
33050
+ * parameter deliberately named after a changed resource merely un-binds one
33051
+ * record — the direction that previews a row rather than hiding one. Pseudo
33052
+ * parameters (`AWS::…`) and the `${!Literal}` escape are dropped.
33053
+ */
33054
+ function referencedLogicalIds(entry) {
33055
+ const ids = /* @__PURE__ */ new Set();
33056
+ const addName = (name) => {
33057
+ const logicalId = name.split(".")[0];
33058
+ if (logicalId !== void 0 && logicalId !== "" && !logicalId.startsWith("AWS::")) ids.add(logicalId);
33059
+ };
33060
+ const walk = (value) => {
33061
+ if (Array.isArray(value)) {
33062
+ for (const item of value) walk(item);
33063
+ return;
33064
+ }
33065
+ if (value === null || typeof value !== "object") return;
33066
+ const obj = value;
33067
+ if (typeof obj["Ref"] === "string") addName(obj["Ref"]);
33068
+ const getAtt = obj["Fn::GetAtt"];
33069
+ if (typeof getAtt === "string") addName(getAtt);
33070
+ else if (Array.isArray(getAtt) && typeof getAtt[0] === "string") addName(getAtt[0]);
33071
+ for (const [key, nested] of Object.entries(obj)) {
33072
+ if (key === "Fn::Sub") {
33073
+ walkSub(nested);
33074
+ continue;
33075
+ }
33076
+ walk(nested);
33077
+ }
33078
+ };
33079
+ /**
33080
+ * `Fn::Sub` in both spellings. Handled here rather than by the generic walk
33081
+ * because the two-argument form's second element is a VARIABLE MAP, not a
33082
+ * template fragment: its KEYS are variable names, so letting the generic
33083
+ * walk see `{ Ref: 'Unrelated' }` there would read a variable literally
33084
+ * named `Ref` as a `Ref` intrinsic. The map's VALUES are walked, since the
33085
+ * resolver resolves them.
33086
+ */
33087
+ const walkSub = (sub) => {
33088
+ const isPair = Array.isArray(sub);
33089
+ const subTemplate = typeof sub === "string" ? sub : isPair ? sub[0] : void 0;
33090
+ const variableMap = isPair && sub[1] !== null && typeof sub[1] === "object" ? sub[1] : void 0;
33091
+ if (typeof subTemplate === "string") {
33092
+ const declaredVars = new Set(variableMap === void 0 ? [] : Object.keys(variableMap));
33093
+ for (const [, placeholder] of subTemplate.matchAll(/\$\{([^}]*)\}/g)) {
33094
+ if (placeholder === void 0 || placeholder.startsWith("!")) continue;
33095
+ if (declaredVars.has(placeholder)) continue;
33096
+ addName(placeholder);
33097
+ }
33098
+ }
33099
+ if (isPair) for (const [index, element] of sub.entries()) {
33100
+ if (index === 1) {
33101
+ if (variableMap !== void 0) for (const declared of Object.values(variableMap)) walk(declared);
33102
+ continue;
33103
+ }
33104
+ walk(element);
33105
+ }
33106
+ else walk(sub);
33107
+ };
33108
+ walk(entry);
33109
+ return ids;
33110
+ }
33111
+ /**
33112
+ * The keys of a `StackState.skippedOutputs` record that still BIND against
33113
+ * today's template: recorded digest equal to the digest of the same key over
33114
+ * `pristineTemplate`, which must be the pre-resolution snapshot the module doc
33115
+ * describes. A key the template no longer declares is skipped outright — the
33116
+ * record is inert for it, and the next deploy clears it. The diff passes the
33117
+ * result to `resolveTemplateOutputs`, which adds the one check only state can
33118
+ * answer — that the key is still absent from the stored bag.
33119
+ *
33120
+ * `changedLogicalIds` is the second half of the binding rule, and the one the
33121
+ * digest structurally cannot carry (see the module doc's `Resources` bullet):
33122
+ * the logical ids this run's resource diff reports as changing. An output
33123
+ * whose entry references one of them does NOT bind, because the deploy that
33124
+ * follows takes the changed-resources path, re-resolves every output, and may
33125
+ * publish the very key the record would have hidden. Omitted by a caller with
33126
+ * no resource diff in hand — every such caller today is a test asserting the
33127
+ * digest half alone.
33128
+ */
33129
+ function bindingSkippedOutputs(pristineTemplate, record, changedLogicalIds) {
33130
+ const binding = /* @__PURE__ */ new Set();
33131
+ if (record === null || record === void 0) return binding;
33132
+ const declared = pristineTemplate.Outputs ?? {};
33133
+ for (const [outputKey, digest] of Object.entries(record)) {
33134
+ if (!Object.prototype.hasOwnProperty.call(declared, outputKey)) continue;
33135
+ const entry = declared[outputKey];
33136
+ if (digest !== skippedOutputDigest(pristineTemplate, outputKey)) continue;
33137
+ if (changedLogicalIds !== void 0 && [...referencedLogicalIds(entry)].some((id) => changedLogicalIds.has(id))) continue;
33138
+ binding.add(outputKey);
33139
+ }
33140
+ return binding;
33141
+ }
33142
+ /**
33143
+ * Whether two records — either possibly absent — describe the same skipped
33144
+ * set with the same digests. The no-change deploy path persists on a
33145
+ * difference here: it is the ONLY save trigger for this field on that path,
33146
+ * because a skipped output switches the outputs-changed trigger off.
33147
+ */
33148
+ function skippedOutputsEqual(a, b) {
33149
+ const aKeys = a === null || a === void 0 ? [] : Object.keys(a);
33150
+ const bKeys = b === null || b === void 0 ? [] : Object.keys(b);
33151
+ if (aKeys.length !== bKeys.length) return false;
33152
+ return aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && a?.[k] === b?.[k]);
33153
+ }
33154
+
32510
33155
  //#endregion
32511
33156
  //#region src/analyzer/implicit-delete-deps.ts
32512
33157
  /**
@@ -33432,7 +34077,8 @@ var ReplayResolvers = class {
33432
34077
  * `state.json` already stores in the clear.
33433
34078
  */
33434
34079
  function regionAmbiguousReplaySecretError(logicalId, propertyPath, secretName, foreignProducerRegions, consumerRegion) {
33435
- return new CdkdError(`Rollback of ${logicalId}${propertyPath === "" ? "" : ` property '${propertyPath}'`} cannot re-resolve the secret reference '${secretName}': the reference carries no region of its own, and this stack read across a region boundary (producer region(s) on record: ${foreignProducerRegions.join(", ")}), so it may have been resolved in one of those rather than in '${consumerRegion}'. A secret of the same name in two regions is two independent values, so replaying this would write the WRONG secret to a live resource. Refusing instead. Resolve the reference in its own region and set the property directly (or spell it as a full ARN, which names its region and is resolved there), then re-run 'cdkd rollback'.`, "ROLLBACK_SECRET_REGION_AMBIGUOUS");
34080
+ const where = propertyPath === "" ? "" : ` property '${propertyPath}'`;
34081
+ return new CdkdError(`Rollback of ${logicalId}${where} cannot re-resolve the secret reference '${secretName}': the reference carries no region of its own, and this stack read across a region boundary (producer region(s) on record: ${foreignProducerRegions.join(", ")}), so it may have been resolved in one of those rather than in '${consumerRegion}'. A secret of the same name in two regions is two independent values, so replaying this would write the WRONG secret to a live resource. Refusing instead. Resolve the reference in its own region and set the property directly (or spell it as a full ARN, which names its region and is resolved there), then re-run 'cdkd rollback'.`, "ROLLBACK_SECRET_REGION_AMBIGUOUS");
33436
34082
  }
33437
34083
  /**
33438
34084
  * Re-resolve one LEAF string, sending each `{{resolve:...}}` reference in it to
@@ -33535,10 +34181,11 @@ async function resolveLeafByRegion(leaf, propertyPath, logicalId, execCtx, resol
33535
34181
  */
33536
34182
  function redactRollbackRecord(record, secrets, journaledProps) {
33537
34183
  if (secrets.size === 0) return record;
33538
- return scrubResourceRecord(journaledProps === void 0 ? record : {
34184
+ const positioned = journaledProps === void 0 ? record : {
33539
34185
  ...record,
33540
34186
  properties: redactSecretsForState(record.properties, secrets, journaledProps, STATE_DERIVED_RULES)
33541
- }, secrets);
34187
+ };
34188
+ return scrubResourceRecord(positioned, secrets);
33542
34189
  }
33543
34190
  async function updateWithRollbackRetry(provider, args, logicalId, logger, isInterrupted, secrets) {
33544
34191
  if (provider.disableOuterRetry) return await withCurrentResourceSecrets(secrets, () => provider.update(...args));
@@ -34759,13 +35406,13 @@ const EMPTY_SECRETS = /* @__PURE__ */ new Map();
34759
35406
  * resource has been in flight for 5 minutes. Most CC API resources
34760
35407
  * complete in under a minute; 5m is the agreed elbow.
34761
35408
  */
34762
- const DEFAULT_RESOURCE_WARN_AFTER_MS = 300 * 1e3;
35409
+ const DEFAULT_RESOURCE_WARN_AFTER_MS = 3e5;
34763
35410
  /**
34764
35411
  * Default per-resource hard timeout: abort after 30 minutes. Matches the
34765
35412
  * design doc — Custom-Resource-heavy stacks should pass `--resource-timeout 1h`
34766
35413
  * explicitly because the Custom Resource provider's polling cap is 1h.
34767
35414
  */
34768
- const DEFAULT_RESOURCE_TIMEOUT_MS = 1800 * 1e3;
35415
+ const DEFAULT_RESOURCE_TIMEOUT_MS = 18e5;
34769
35416
  var InterruptedError = class extends Error {
34770
35417
  constructor(reason = "user") {
34771
35418
  super(reason === "user" ? "Deployment interrupted by user (Ctrl+C)" : "Deployment aborted after another resource failed");
@@ -35021,8 +35668,17 @@ var DeployEngine = class {
35021
35668
  * secret collapse onto whichever expression was recorded last, exactly as two
35022
35669
  * resource properties did before #1904. Captured in `resolveOutputs` at the
35023
35670
  * same point `outputSecrets` is accumulated. Reset per `deploy()`.
35671
+ *
35672
+ * Null-prototype for the same reason as the outputs bag `resolveOutputs`
35673
+ * builds two lines away: both are keyed by TEMPLATE-CONTROLLED output names,
35674
+ * so an output called `__proto__` would replace this bag's prototype and a
35675
+ * later lookup of a name it never stored would inherit from it. Only the
35676
+ * RESET in `deploy()` is pinned — reverting this initialiser alone survives
35677
+ * the suite, measured, because `deploy()` replaces the bag before anything
35678
+ * writes to it, so the initialiser matches the reset rather than standing as
35679
+ * a second guard.
35024
35680
  */
35025
- outputsTemplateSource = {};
35681
+ outputsTemplateSource = Object.create(null);
35026
35682
  /**
35027
35683
  * The export aliases the last `resolveOutputs` pass WROTE into its bag
35028
35684
  * (issue #2193) — exactly the keys `outputs[exportName] = value` landed on,
@@ -35035,6 +35691,18 @@ var DeployEngine = class {
35035
35691
  */
35036
35692
  resolvedExportNames = [];
35037
35693
  /**
35694
+ * The outputs the last `resolveOutputs` pass could NOT resolve and SKIPPED
35695
+ * (the resolver threw under the default arm of
35696
+ * `handleOutputResolutionFailure`, or returned `undefined` outright — see
35697
+ * `collectSkippedOutputs`), each mapped to the
35698
+ * digest `cdkd diff` compares against (issue #2740, `StackState.
35699
+ * skippedOutputs`). `undefined` when nothing was skipped, so the saves that
35700
+ * persist the resolved bag spread it as-is and the field stays omitted. Same
35701
+ * lifetime rule as `resolvedExportNames`: reset at the top of every
35702
+ * `resolveOutputs`, meaningful only right after that call returns.
35703
+ */
35704
+ skippedOutputs;
35705
+ /**
35038
35706
  * Whether {@link outputsTemplateSource} may be used to POSITION the outputs
35039
35707
  * redaction. False once an outputs pass threw partway: the post-loop
35040
35708
  * name pass never ran, so the bag holds only the alias keys written before
@@ -35104,7 +35772,7 @@ var DeployEngine = class {
35104
35772
  });
35105
35773
  this.options.concurrency = options.concurrency ?? 10;
35106
35774
  this.options.dryRun = options.dryRun ?? false;
35107
- this.options.lockTimeout = options.lockTimeout ?? 300 * 1e3;
35775
+ this.options.lockTimeout = options.lockTimeout ?? 3e5;
35108
35776
  this.options.noRollback = options.noRollback ?? false;
35109
35777
  this.options.resourceWarnAfterMs = options.resourceWarnAfterMs ?? 3e5;
35110
35778
  this.options.resourceTimeoutMs = options.resourceTimeoutMs ?? 18e5;
@@ -35119,9 +35787,11 @@ var DeployEngine = class {
35119
35787
  this.perResourceSecrets = /* @__PURE__ */ new Map();
35120
35788
  this.noEchoAttributeResources = /* @__PURE__ */ new Map();
35121
35789
  this.perResourceTemplateProps = /* @__PURE__ */ new Map();
35790
+ this.attemptedResolvedProps = /* @__PURE__ */ new Map();
35122
35791
  this.outputSecrets = /* @__PURE__ */ new Map();
35123
- this.outputsTemplateSource = {};
35792
+ this.outputsTemplateSource = Object.create(null);
35124
35793
  this.outputsSourceUsable = true;
35794
+ this.skippedOutputs = void 0;
35125
35795
  this.retainedOldOnReplacement = /* @__PURE__ */ new Set();
35126
35796
  this.resolver.resetPhysicalIdFallbackCount();
35127
35797
  return withStackName(stackName, () => this.doDeploy(stackName, template));
@@ -35339,7 +36009,7 @@ var DeployEngine = class {
35339
36009
  const ownSecrets = secrets ?? /* @__PURE__ */ new Map();
35340
36010
  const next = { ...op };
35341
36011
  if (next.properties) next.properties = redactSecretsForState(next.properties, ownSecrets, templateProps);
35342
- if (next.attemptedProperties) next.attemptedProperties = redactSecretsForState(next.attemptedProperties, ownSecrets, templateProps);
36012
+ if (next.attemptedProperties) next.attemptedProperties = redactSecretsForState(markSameGenerationBag({ ...next.attemptedProperties }), ownSecrets, templateProps);
35343
36013
  if (next.previousState) next.previousState = scrubResourceRecord(next.previousState, ownSecrets);
35344
36014
  return next;
35345
36015
  });
@@ -35427,7 +36097,7 @@ var DeployEngine = class {
35427
36097
  const logicalId = entries[i][0];
35428
36098
  const observed = resolved[i];
35429
36099
  const target = stateResources[logicalId];
35430
- if (target && observed !== void 0) target.observedProperties = observed;
36100
+ if (target && observed !== void 0) target.observedProperties = markSameGenerationBag({ ...observed });
35431
36101
  }
35432
36102
  }
35433
36103
  /**
@@ -35605,6 +36275,10 @@ var DeployEngine = class {
35605
36275
  } catch {}
35606
36276
  this.kickOffAutoRefreshObservedProperties(currentState.resources);
35607
36277
  this.logger.debug(`Template has ${Object.keys(template.Resources || {}).length} resources`);
36278
+ const outputsDigestSource = structuredClone({
36279
+ ...template,
36280
+ Resources: {}
36281
+ });
35608
36282
  const parameterValues = await this.resolver.resolveParameters(template, this.options.parameters, { ...this.options.inheritedSecrets && this.options.inheritedSecrets.size > 0 && { inheritedSecrets: this.options.inheritedSecrets } });
35609
36283
  this.logger.debug(`Resolved ${Object.keys(parameterValues).length} parameters: ${Object.keys(parameterValues).join(", ")}`);
35610
36284
  const context = this.buildResolverContext({
@@ -35648,7 +36322,7 @@ var DeployEngine = class {
35648
36322
  this.logger.info("No changes detected. Stack is up to date.");
35649
36323
  let persistedOutputs = currentState.outputs ?? {};
35650
36324
  if (!this.options.dryRun) {
35651
- const resolvedOutputs = this.redactOutputs(await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, parameterValues, conditions));
36325
+ const resolvedOutputs = this.redactOutputs(await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, outputsDigestSource, parameterValues, conditions));
35652
36326
  const resolutionFailed = Object.values(resolvedOutputs).some((v) => v === void 0);
35653
36327
  const outputsChanged = !resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs);
35654
36328
  const currentEffectiveExports = new Set(importableOutputKeys(currentState));
@@ -35657,7 +36331,8 @@ var DeployEngine = class {
35657
36331
  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.");
35658
36332
  const observedRefresh = this.observedCaptureTasks.size > 0;
35659
36333
  if (observedRefresh) await this.drainObservedCaptures(currentState.resources);
35660
- if (observedRefresh || outputsChanged || exportSetChanged) try {
36334
+ const skippedOutputsChanged = !skippedOutputsEqual(currentState.skippedOutputs, this.skippedOutputs);
36335
+ if (observedRefresh || outputsChanged || exportSetChanged || skippedOutputsChanged) try {
35661
36336
  const refreshedState = {
35662
36337
  version: 9,
35663
36338
  region: this.stackRegion,
@@ -35665,6 +36340,7 @@ var DeployEngine = class {
35665
36340
  resources: currentState.resources,
35666
36341
  outputs: outputsChanged ? resolvedOutputs : persistedOutputs,
35667
36342
  ...resolutionFailed ? exportNamesCarriedFrom(currentState) : { exportNames: [...this.resolvedExportNames] },
36343
+ ...this.skippedOutputs && { skippedOutputs: { ...this.skippedOutputs } },
35668
36344
  ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
35669
36345
  lastModified: Date.now()
35670
36346
  };
@@ -35677,7 +36353,8 @@ var DeployEngine = class {
35677
36353
  if (outputsChanged) this.logger.info("Persisted Outputs-only change (no resource diff).");
35678
36354
  else this.logger.debug("Persisted export-set change (no outputs-value diff, no-change path, #2193)");
35679
36355
  if (this.exportIndexStore) await this.exportIndexStore.updateForStack(stackName, this.stackRegion, importableOutputs(refreshedState));
35680
- } else this.logger.debug("Persisted refreshed observedProperties (no-change path)");
36356
+ } else if (observedRefresh) this.logger.debug("Persisted refreshed observedProperties (no-change path)");
36357
+ else this.logger.debug("Persisted skipped-outputs record (no outputs-value diff, no-change path, #2740)");
35681
36358
  } catch (saveError) {
35682
36359
  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.`);
35683
36360
  }
@@ -35719,7 +36396,7 @@ var DeployEngine = class {
35719
36396
  current: 0,
35720
36397
  total: createChanges.length + updateChanges.length + deleteChanges.length
35721
36398
  };
35722
- const { state: newState, actualCounts } = await this.executeDeployment(effectiveTemplate, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress, migrationPending);
36399
+ const { state: newState, actualCounts } = await this.executeDeployment(effectiveTemplate, currentState, changes, dag, executionLevels, stackName, outputsDigestSource, parameterValues, conditions, currentEtag, progress, migrationPending);
35723
36400
  await this.drainObservedCaptures(newState.resources);
35724
36401
  const newEtag = await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(newState));
35725
36402
  this.logger.debug(`State saved (ETag: ${newEtag})`);
@@ -35761,7 +36438,7 @@ var DeployEngine = class {
35761
36438
  * - DELETE follows reverse dependency order (a node starts as soon as all
35762
36439
  * resources that depend ON it have finished deleting)
35763
36440
  */
35764
- async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress, migrationPending = false) {
36441
+ async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, outputsDigestSource, parameterValues, conditions, currentEtag, progress, migrationPending = false) {
35765
36442
  const concurrency = this.options.concurrency;
35766
36443
  const newResources = { ...currentState.resources };
35767
36444
  const actualCounts = {
@@ -35787,6 +36464,7 @@ var DeployEngine = class {
35787
36464
  resources: newResources,
35788
36465
  outputs: currentState.outputs,
35789
36466
  ...exportNamesCarriedFrom(currentState),
36467
+ ...skippedOutputsCarriedFrom(currentState),
35790
36468
  ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
35791
36469
  lastModified: Date.now()
35792
36470
  };
@@ -35919,6 +36597,7 @@ var DeployEngine = class {
35919
36597
  resources: newResources,
35920
36598
  outputs: currentState.outputs,
35921
36599
  ...exportNamesCarriedFrom(currentState),
36600
+ ...skippedOutputsCarriedFrom(currentState),
35922
36601
  ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
35923
36602
  lastModified: Date.now()
35924
36603
  };
@@ -35955,6 +36634,7 @@ var DeployEngine = class {
35955
36634
  resources: newResources,
35956
36635
  outputs: currentState.outputs,
35957
36636
  ...exportNamesCarriedFrom(currentState),
36637
+ ...skippedOutputsCarriedFrom(currentState),
35958
36638
  ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
35959
36639
  lastModified: Date.now()
35960
36640
  };
@@ -35972,6 +36652,7 @@ var DeployEngine = class {
35972
36652
  resources: newResources,
35973
36653
  outputs: currentState.outputs,
35974
36654
  ...exportNamesCarriedFrom(currentState),
36655
+ ...skippedOutputsCarriedFrom(currentState),
35975
36656
  ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
35976
36657
  lastModified: Date.now()
35977
36658
  };
@@ -35986,7 +36667,7 @@ var DeployEngine = class {
35986
36667
  }
35987
36668
  let outputs;
35988
36669
  try {
35989
- outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
36670
+ outputs = await this.resolveOutputs(template, newResources, stackName, outputsDigestSource, parameterValues, conditions);
35990
36671
  const resolvedOutputsBeforeRedaction = outputs;
35991
36672
  outputs = this.redactOutputs(outputs);
35992
36673
  this.rememberRecoverableMaskedOutputs(stackName, resolvedOutputsBeforeRedaction, outputs);
@@ -36003,6 +36684,7 @@ var DeployEngine = class {
36003
36684
  resources: newResources,
36004
36685
  outputs,
36005
36686
  exportNames: [...this.resolvedExportNames],
36687
+ ...this.skippedOutputs && { skippedOutputs: { ...this.skippedOutputs } },
36006
36688
  ...this.recordedImports.length > 0 && { imports: [...this.recordedImports] },
36007
36689
  ...this.recordedOutputReads.length > 0 && { outputReads: [...this.recordedOutputReads] },
36008
36690
  lastModified: Date.now()
@@ -36041,6 +36723,7 @@ var DeployEngine = class {
36041
36723
  resources: newResources,
36042
36724
  outputs: currentState.outputs,
36043
36725
  ...exportNamesCarriedFrom(currentState),
36726
+ ...skippedOutputsCarriedFrom(currentState),
36044
36727
  ...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
36045
36728
  lastModified: Date.now()
36046
36729
  });
@@ -36361,7 +37044,8 @@ var DeployEngine = class {
36361
37044
  if (!redirect) return;
36362
37045
  const findings = findUnrewrittenAssetReferences(resolvedProps, redirect);
36363
37046
  if (findings.length === 0) return;
36364
- throw new ProvisioningError(`Unrewritten asset reference on '${logicalId}' (${resourceType}): this region uses cdkd-owned asset storage, but the following resolved properties still point at the CDK bootstrap storage that 'cdk gc' may garbage-collect:\n${findings.map((f) => ` - ${f.path}: still references '${f.source}'`).join("\n")}\nThis is a template shape cdkd's asset-reference rewrite did not cover — deploying it would split-brain the stack (assets in cdkd storage, properties reading the CDK bucket). Please report this at https://github.com/go-to-k/cdkd/issues with the property shape. Workaround: deploy with --use-cdk-bootstrap-assets to pin the legacy destinations for this app.`, resourceType, logicalId);
37047
+ const detail = findings.map((f) => ` - ${f.path}: still references '${f.source}'`).join("\n");
37048
+ throw new ProvisioningError(`Unrewritten asset reference on '${logicalId}' (${resourceType}): this region uses cdkd-owned asset storage, but the following resolved properties still point at the CDK bootstrap storage that 'cdk gc' may garbage-collect:\n${detail}\nThis is a template shape cdkd's asset-reference rewrite did not cover — deploying it would split-brain the stack (assets in cdkd storage, properties reading the CDK bucket). Please report this at https://github.com/go-to-k/cdkd/issues with the property shape. Workaround: deploy with --use-cdk-bootstrap-assets to pin the legacy destinations for this app.`, resourceType, logicalId);
36365
37049
  }
36366
37050
  /**
36367
37051
  * The `Snapshot`-policy gate every engine delete site runs BEFORE its
@@ -36503,7 +37187,7 @@ var DeployEngine = class {
36503
37187
  recordNestedStackParameterExpressions(updateSecrets, resourceType, resolvedProps, desiredProps);
36504
37188
  this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
36505
37189
  this.attemptedResolvedProps.set(logicalId, resolvedProps);
36506
- if (JSON.stringify(redactSecretsForState(resolvedProps, updateSecrets, desiredProps)) === JSON.stringify(currentPropsAsWritten)) {
37190
+ if (JSON.stringify(redactSecretsForState(markSameGenerationBag({ ...resolvedProps }), updateSecrets, desiredProps)) === JSON.stringify(currentPropsAsWritten)) {
36507
37191
  if (change.attributeChanges && change.attributeChanges.length > 0) {
36508
37192
  const attrSummary = change.attributeChanges.map((a) => `${a.attribute}: ${a.oldValue ?? "(unset)"} → ${a.newValue ?? "(unset)"}`).join(", ");
36509
37193
  this.logger.info(` ↻ ${logicalId} (${resourceType}) attribute update: ${attrSummary}`);
@@ -36733,7 +37417,8 @@ var DeployEngine = class {
36733
37417
  createResult = await this.withRetry(() => withCurrentResourceSecrets(updateSecrets, () => replProvider.create(logicalId, resourceType, replProps, { maskSecrets: createSecretMasker(updateSecrets) })), logicalId, void 0, void 0, replProvider);
36734
37418
  } catch (createError) {
36735
37419
  if (!retainOldOnReplace) throw new Error(maskSecretsInText(`Failed to create ${logicalId} after the UPDATE-not-supported replacement: the old resource (${currentResource.physicalId}) is now gone. Cause: ${createError instanceof Error ? createError.message : String(createError)}. Re-run the deploy to create it fresh.`, updateSecrets), { cause: createError instanceof Error ? createError : void 0 });
36736
- if (!isNameCollisionError(createError instanceof Error ? createError.message : String(createError))) throw createError;
37420
+ const createMsg = createError instanceof Error ? createError.message : String(createError);
37421
+ if (!isNameCollisionError(createMsg)) throw createError;
36737
37422
  const nameOrigin = this.replacementNameOrigin(logicalId, currentResource.physicalId);
36738
37423
  throw markNonRetryable(new CdkdError(`${logicalId} (${resourceType}) requires replacement because the provisioning layer cannot update it in place — but its physical name is still held by the existing resource AND UpdateReplacePolicy: Retain pins that resource in place. ${nameOrigin.descriptor}. ${nameOrigin.remedy} — with Retain, the old resource keeps the name, so a same-name replacement can never proceed. Removing UpdateReplacePolicy: Retain lets cdkd delete the old resource first, which destroys it and any data it holds.`, "NAMED_REPLACEMENT_COLLISION", createError instanceof Error ? createError : void 0));
36739
37424
  }
@@ -36782,8 +37467,10 @@ var DeployEngine = class {
36782
37467
  const updateCaptureSiblings = await this.buildObservedCaptureSiblings(resourceType, logicalId, result.physicalId, template, stateResources, stackName, parameterValues, conditions);
36783
37468
  this.kickOffObservedCapture(captureProvider, logicalId, result.physicalId, resourceType, resolvedProps, updateCaptureSiblings);
36784
37469
  const updatePartial = updatePartialReason(result);
36785
- if (counts) if (updatePartial !== void 0) counts.updatePartial++;
36786
- else counts.updated++;
37470
+ if (counts) {
37471
+ if (updatePartial !== void 0) counts.updatePartial++;
37472
+ else counts.updated++;
37473
+ }
36787
37474
  if (progress) progress.current++;
36788
37475
  const updatePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
36789
37476
  renderer.removeTask(logicalId);
@@ -36903,9 +37590,9 @@ var DeployEngine = class {
36903
37590
  * `cdkd export`, `cdkd state`.
36904
37591
  */
36905
37592
  propertiesToRecord(desiredProperties, result, resourceType, provisionedBy) {
36906
- const effective = result.effectiveProperties ?? desiredProperties;
36907
- if (provisionedBy !== "sdk") return effective;
36908
- return withoutSilentDropProperties(resourceType, effective);
37593
+ if (result.effectiveProperties) return provisionedBy === "sdk" ? withoutSilentDropProperties(resourceType, result.effectiveProperties) : result.effectiveProperties;
37594
+ const written = provisionedBy === "sdk" ? withoutSilentDropProperties(resourceType, desiredProperties) : desiredProperties;
37595
+ return markSameGenerationBag(written);
36909
37596
  }
36910
37597
  /**
36911
37598
  * The name-origin half of the replacement-collision messages (issue #1636).
@@ -37217,10 +37904,10 @@ var DeployEngine = class {
37217
37904
  * rather than whatever the declaration order happened to have recorded by
37218
37905
  * then (issue #1919).
37219
37906
  */
37220
- async resolveOutputs(template, resources, stackName, parameterValues, conditions) {
37907
+ async resolveOutputs(template, resources, stackName, digestSource, parameterValues, conditions) {
37221
37908
  this.resolvedExportNames = [];
37222
37909
  if (!template.Outputs) return {};
37223
- const outputs = {};
37910
+ const outputs = Object.create(null);
37224
37911
  const context = this.buildResolverContext({
37225
37912
  template,
37226
37913
  resources,
@@ -37285,7 +37972,8 @@ var DeployEngine = class {
37285
37972
  if (!publishedOutputNames.has(outputKey)) continue;
37286
37973
  this.outputsTemplateSource[outputKey] = output.Value;
37287
37974
  }
37288
- return outputs;
37975
+ this.skippedOutputs = collectSkippedOutputs(digestSource, outputs);
37976
+ return markSameGenerationBag(outputs);
37289
37977
  }
37290
37978
  buildDisplayOutputs(template, resolvedOutputs) {
37291
37979
  const display = {};
@@ -37299,5 +37987,5 @@ var DeployEngine = class {
37299
37987
  };
37300
37988
 
37301
37989
  //#endregion
37302
- 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 };
37303
- //# sourceMappingURL=deploy-engine-ZPkBIilF.js.map
37990
+ export { ProviderRegistry as $, redactDockerArgvValues as $n, ResourceUpdateNotSupportedError as $r, STATE_SOURCED_READBACK_RULES as $t, renderStatefulReason as A, createAssetRedirectResolver as An, AssemblyReader as Ar, configStringRefusal as At, red as B, readBootstrapMarkerBody as Bn, ConfigError as Br, s3BucketDualStackDomainName as Bt, refusesFinalSnapshot as C, importableOutputKeys as Cn, uploadCfnTemplate as Cr, refStateLookupFromResource as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, stringifyValue as Dn, PARTITION_TABLE as Dr, assertRegionMatch as Dt, extractDeploymentEventError as E, AssetPublisher as En, expectedOwnerParam as Er, resolveExplicitPhysicalId as Et, formatResourceLine as F, assertAssetBucketRegion as Fn, getAwsClients as Fr, requireConfigString as Ft, isExportAliasCollision as G, buildDockerImage as Gn, IntrinsicResolutionRefusalError as Gr, INTRINSIC_KEYS as Gt, collectDeclaredOutputNames as H, validateContainerRepoName as Hn, DependencyError as Hr, s3BucketWebsiteUrl as Ht, bold as I, ensureAssetStorage as In, resetAwsClients as Ir, classifyReplaySecretRegion as It, stateKeySecretExposure as J, describeDockerFailure as Jn, LockError as Jr, describeTypeWithThrottleRetry as Jt, secretBearingStateKeyWarning as K, describeDockerCapturedOutput as Kn, LocalInvokeBuildError as Kr, findActionableSilentDrops as Kt, cyan as L, getBootstrapMarkerKey as Ln, setAwsClients as Lr, producerRegionsFromState as Lt, coerceWarmThroughput as M, rewriteTemplateAssetReferences as Mn, clearBucketRegionCache as Mr, replayWarn as Mt, isWarmThroughputDecrease as N, AssetModeResolver as Nn, resolveBucketRegion as Nr, requireConfigArray as Nt, isStatefulRecreateTargetForReplace as O, WorkGraph as On, canonicalizeRegion as Or, coerceCfnBoolean as Ot, toFiniteNumber as P, BOOTSTRAP_MARKER_PREFIX as Pn, AwsClients as Pr, requireConfigObject as Pt, clearOnUpdateRemoval as Q, partitionSensitiveEnv as Qn, ResourceTimeoutError as Qr, STATE_SOURCED_CROSS_GENERATION_RULES as Qt, gray as R, isCrossRegionRedirect as Rn, AssetError as Rr, s3BucketArn as Rt, isFinalSnapshotError as S, exportNamesCarriedFrom as Sn, findLargeInlineResources as Sr, parameterTypeMayLoseSecretIdentity as St, makeCanonicalizePropertiesFn as T, shouldRetainResource as Tn, displaySafe as Tr, normalizeAwsTagsToCfn as Tt, collectPublishedOutputNames as U, buildDenyExternalAccessPolicy as Un, DeployCancelledError as Ur, applyRoleArnIfSet as Ut, yellow as V, validateAssetBucketName as Vn, CrossAccountSecretRefusalError as Vr, s3BucketRegionalDomainName as Vt, exportAliasCollisionScrubWarning as W, describeAwsFailure as Wn, DynamicReferenceRegionAmbiguousError as Wr, DiffCalculator as Wt, IAMRoleProvider as X, formatDockerLoginError as Xn, PartialFailureError as Xr, DagBuilder as Xt, getCurrentResourceSecrets as Y, dockerSpawnEnvWithSensitive as Yn, NestedStackChildDirectDestroyError as Yr, withRetry as Yt, collectInlinePolicyNamesManagedBySiblings as Z, getDockerCmd as Zn, ProvisioningError as Zr, TemplateParser as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, buildLockContentionMessage as _n, stateBucketExistenceConfirmed as _r, carriesDynamicReference as _t, DeploymentEventsStore as a, isCdkdError as ai, isSingleDynamicReferenceToken as an, getDockerImageBySourceHash as ar, endCommandInterruptScope as at, ccRoutedFinalSnapshotError as b, CUSTOM_RESOURCE_RESPONSE_PREFIX as bn, CFN_TEMPLATE_URL_LIMIT as br, getAccountInfo as bt, replayFailedOperations as c, isMarkedNonRetryable as ci, recordMaskOnlyValue as cn, getDefaultStateBucketName as cr, startInterruptWatch as ct, updatePartialReason as d, isTransientServerError as di, scrubResourceRecord as dn, resolveAutoAssetStorage as dr, UNSPECIFIED_SKIP_REASON as dt, StackHasActiveImportsError as ei, TEMPLATE_SOURCED_RULES as en, runDockerForeground as er, wouldReturnToSdkProvider as et, withResourceDeadline as f, markNonRetryable as fi, LockManager as fn, resolveCaptureObservedState as fr, deleteIndeterminateGuards as ft, bindingSkippedOutputs as g, buildForceUnlockCommand as gn, resolveUseCdkBootstrapAssets as gr, IntrinsicFunctionResolver as gt, computeImplicitDeleteEdges as h, __exportAll as hi, UNRENDERABLE as hn, resolveStateBucketWithDefaultAndSource as hr, isTerminationProtectionPropagationError as ht, DeploymentEventsReader as i, formatError as ii, errorCauseChain as in, AssetManifestLoader as ir, beginCommandInterruptScope as it, WARM_THROUGHPUT_MEMBERS as j, loadPublishableAssetManifest as jn, processStackMessages as jr, readConfigString as jt, isStatefulRecreateTargetSync as k, buildAssetRedirectMap as kn, derivePartitionAndUrlSuffix as kr, configBooleanRefusal as kt, replayRollback as l, isRetryableTransientError as li, recoverMaskedOutput as ln, getLegacyStateBucketName as lr, CloudControlProvider as lt, IMPLICIT_DELETE_DEPENDENCIES as m, retryClassificationText as mi, rebuildClientForBucketRegion as mn, resolveStateBucketWithDefault as mr, disableInstanceApiTermination as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, StateError as ni, createSecretMasker as nn, escapeRegExp$1 as nr, maskDeep as nt, planFailedOps as o, normalizeAwsError as oi, maskSecretsInError as on, Synthesizer as or, interruptWatchListenerCount as ot, maskingRetryLogger as p, markRedactedCause as pi, S3StateBackend as pn, resolveSkipPrefix as pr, deleteSkipReason as pt, secretSafeKeyDisplay as q, describeDockerExecFailure as qn, LocalStartServiceError as qr, findSilentDropProperties as qt, DeployEngine as r, SynthesisError as ri, dynamicReferenceTokens as rn, stripControlChars as rr, maskerOrIdentity as rt, planRollback as s, withErrorHandling as si, maskSecretsInText as sn, synthesisStatusMessage as sr, isInterruptedWaitError as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, StackTerminationProtectionError as ti, carriesSecretMask as tn, runDockerStreaming as tr, createMaskedRetryLogger as tt, updatePartialMessage as u, isThrottlingError as ui, redactSecretsForState as un, resolveApp as ur, slowCcOperationTimeoutMs as ut, PRE_DELETE_SNAPSHOT_TYPES as v, forceQuitRecoveryClause as vn, warnDeprecatedNoPrefixCliFlag as vr, cfnRefValueFromPhysicalId as vt, unsupportedFinalSnapshotError as w, importableOutputs as wn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as wr, WAFv2WebACLProvider as wt, createPreDeleteFinalSnapshot as x, DEFAULT_STATE_PREFIX as xn, MIGRATE_TMP_PREFIX as xr, isUnboundTemplateParameter as xt, buildFinalSnapshotIdentifier as y, shellQuote as yn, CFN_TEMPLATE_BODY_LIMIT as yr, coerceParameterTypedValue as yt, green as z, parseBootstrapMarker as zn, CdkdError as zr, s3BucketDomainName as zt };
37991
+ //# sourceMappingURL=deploy-engine-CfxcC3q3.js.map