@go-to-k/cdkd 0.285.13 → 0.285.15

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,6 +1,6 @@
1
1
  import { _ as withStackName, d as applyDefaultNameForFallback, f as generateResourceName, h as looksLikeCdkdGeneratedName, m as getCurrentStackName, n as getLogger, p as generateResourceNameWithFallback, r as isStdoutReservedForPayload, s as getLiveRenderer } from "./logger-Dw-3y48G.js";
2
2
  import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
3
- import { t as getCdkdVersion } from "./version-FLW4rE5A.js";
3
+ import { t as getCdkdVersion } from "./version-Dcdouqd3.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -7927,6 +7927,140 @@ function parseRollbackJournal(bodyString, stackName) {
7927
7927
  };
7928
7928
  }
7929
7929
 
7930
+ //#endregion
7931
+ //#region src/state/state-prefix.ts
7932
+ /**
7933
+ * The default S3 key prefix for cdkd state.
7934
+ *
7935
+ * Homed in the STATE layer rather than in `src/cli/commands/state-file-keys.ts`
7936
+ * (which re-exports it, so its four existing importers are unchanged) because
7937
+ * `src/state/lock-contention-message.ts` needs it to decide whether a recovery
7938
+ * hint should spell `--state-prefix` at all, and a `src/state/**` module
7939
+ * importing from `src/cli/commands/**` inverts the layering — the CLI sits
7940
+ * ABOVE the state layer in the 7-layer architecture, not below it.
7941
+ *
7942
+ * Note this is only the DEFAULT. Other commands accept `--state-prefix`, so
7943
+ * whole-bucket listings deliberately do not scope to it.
7944
+ */
7945
+ const DEFAULT_STATE_PREFIX = "cdkd";
7946
+ /**
7947
+ * The state-bucket prefix `CustomResourceProvider` PUTs its response
7948
+ * placeholders under, one object per invocation
7949
+ * (`custom-resource-responses/{requestId}.json`).
7950
+ *
7951
+ * Homed here for the same layering reason as {@link DEFAULT_STATE_PREFIX}: the
7952
+ * PRODUCER is `src/provisioning/providers/custom-resource-provider.ts` and the
7953
+ * COLLECTOR is `src/cli/commands/gc.ts`, so a copy in either would be a copy
7954
+ * the other could drift from — and the two spellings would then disagree about
7955
+ * which objects exist, which is the only way a sweeper can miss the family it
7956
+ * was written for (issue #2052). `src/cli/commands/state-file-keys.ts`
7957
+ * re-exports it so gc reads it alongside the other state-key constants.
7958
+ *
7959
+ * Note this is only the DEFAULT: `ProviderRegistry` can be configured with a
7960
+ * different `responsePrefix`, so a sweep scoped to this value is a sweep of the
7961
+ * default layout. gc has no access to a non-default one — nothing persists it —
7962
+ * which is stated at the sweep's own call site rather than implied here.
7963
+ */
7964
+ const CUSTOM_RESOURCE_RESPONSE_PREFIX = "custom-resource-responses";
7965
+
7966
+ //#endregion
7967
+ //#region src/state/lock-contention-message.ts
7968
+ /**
7969
+ * The message every fail-fast lock-contention site raises.
7970
+ *
7971
+ * Issue #2161 made six commands throw on `acquireLock` returning `false`
7972
+ * instead of running under a foreign lock. Issue #2170 is the follow-up: the
7973
+ * message those sites raise asked the user to decide "is another process
7974
+ * active?" while printing none of the evidence that would answer it, and the
7975
+ * recovery command it suggested could resolve against the wrong AWS account.
7976
+ *
7977
+ * Both matter more than they look, because of WHICH locks reach this message.
7978
+ * `LockManager.acquireLock` reaps an EXPIRED foreign lock and retries, so a
7979
+ * `false` return means the lock is LIVE — in practice a running `cdkd deploy`.
7980
+ * The user who follows a bare `cdkd force-unlock <stack>` suggestion therefore
7981
+ * deletes a live owner's lock and reproduces issue #2161's harm by hand, which
7982
+ * is the outcome #2161 exists to prevent.
7983
+ *
7984
+ * Centralising the text here also settles the third finding: the nine sites
7985
+ * had drifted to three spellings (`for stack` / `for nested stack` /
7986
+ * `for nested-stack child`), so a user grepping CI logs for one of them found
7987
+ * two of three. `subject` now varies only the noun.
7988
+ */
7989
+ /**
7990
+ * Stand-in for a value with nothing renderable left after sanitization. Named
7991
+ * rather than inlined so the message and the command-suppression branch cannot
7992
+ * disagree about what "unrenderable" looks like.
7993
+ */
7994
+ const UNRENDERABLE = "<unrenderable>";
7995
+ /** Render `expiresIn` without implying more precision than a clock skew allows. */
7996
+ function formatRemaining(ms) {
7997
+ if (!Number.isFinite(ms)) return "at an unknown time";
7998
+ if (ms <= 0) return "already expired";
7999
+ const minutes = Math.round(ms / 6e4);
8000
+ if (minutes < 1) return "in under a minute";
8001
+ return `in ~${minutes}m`;
8002
+ }
8003
+ function shellQuote(value) {
8004
+ return /^[A-Za-z0-9._/@:+-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
8005
+ }
8006
+ /**
8007
+ * The `cdkd force-unlock ...` line, carrying every flag that decides which
8008
+ * lock object it resolves to.
8009
+ */
8010
+ function buildForceUnlockCommand(stackName, region, recovery) {
8011
+ const safeStack = displaySafe(stackName, { asciiOnly: true });
8012
+ const stackIsExact = safeStack === stackName;
8013
+ const safeRegion = region === void 0 ? void 0 : displaySafe(region, { asciiOnly: true });
8014
+ if (!safeStack || safeRegion === "" || !stackIsExact || !(region === void 0 || safeRegion === region)) return "";
8015
+ const parts = [safeRegion === void 0 ? `cdkd force-unlock ${shellQuote(safeStack)}` : `cdkd force-unlock ${shellQuote(safeStack)} --stack-region ${shellQuote(safeRegion)}`];
8016
+ if (recovery?.profile) parts.push(`--profile ${shellQuote(recovery.profile)}`);
8017
+ if (recovery?.stateBucket) parts.push(`--state-bucket ${shellQuote(recovery.stateBucket)}`);
8018
+ if (recovery?.statePrefix && recovery.statePrefix !== "cdkd") parts.push(`--state-prefix ${shellQuote(recovery.statePrefix)}`);
8019
+ return parts.join(" ");
8020
+ }
8021
+ /**
8022
+ * The force-quit banner's recovery sentence.
8023
+ *
8024
+ * Exported so the two `destroy-runner.ts` banners do not each decide what to
8025
+ * say when {@link buildForceUnlockCommand} suppresses — a banner ending in a
8026
+ * bare `run: ` is the shape the review found, and two copies of the branch is
8027
+ * how the next one drifts. Returns a leading-space clause so the caller can
8028
+ * concatenate it unconditionally.
8029
+ */
8030
+ function forceQuitRecoveryClause(stackName, region, recovery) {
8031
+ const command = buildForceUnlockCommand(stackName, region, recovery);
8032
+ return command ? ` If the next run reports a lock, run: ${command}` : " Inspect the lock object directly: the name or region recorded for this stack cannot be reproduced safely on a command line, so any command shown here would address a different lock.";
8033
+ }
8034
+ /**
8035
+ * Build the contention message, reading the holder's identity best-effort.
8036
+ *
8037
+ * The `getLockInfo` read is one GetObject and is deliberately NOT allowed to
8038
+ * fail the command: the caller is already on its way to throwing, and turning
8039
+ * a contention refusal into an S3 error would lose the reason. A failed or
8040
+ * absent read degrades to the evidence-free wording rather than to a crash.
8041
+ */
8042
+ async function buildLockContentionMessage(args) {
8043
+ const { lockManager, stackName, region, subject = "stack", recovery, heldClause, suffix } = args;
8044
+ let held = heldClause ?? "another cdkd process holds it";
8045
+ let sawHolder = false;
8046
+ try {
8047
+ const info = await lockManager.getLockInfo(stackName, region);
8048
+ if (info) {
8049
+ const operation = info.operation ? `, operation: ${displaySafe(info.operation)}` : "";
8050
+ const expires = formatRemaining(info.expiresAt - Date.now());
8051
+ const owner = displaySafe(info.owner);
8052
+ const holder = owner ? `held by ${owner}${operation}` : `held by an unnamed holder`;
8053
+ held = `${heldClause ? `${heldClause} — ` : ""}${holder}, expires ${expires}`;
8054
+ if (owner) sawHolder = true;
8055
+ }
8056
+ } catch {}
8057
+ const advice = sawHolder ? `That process is still running — wait for it to finish. Only if you are certain it is gone` : `Wait for it to finish, or if you are certain no other process is active`;
8058
+ const recoveryCommand = buildForceUnlockCommand(stackName, region, recovery);
8059
+ const head = `Could not acquire lock for ${subject} '${displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>"}' (${displaySafe(region, { asciiOnly: true }) || "<unrenderable>"}) — ${held}.` + (suffix ? ` ${suffix}` : "");
8060
+ if (!recoveryCommand) return `${head} ${advice}. No recovery command can be shown: the name or region recorded for this lock contains characters that cannot be reproduced safely on a command line, so any command shown here would address a different lock — inspect the lock object directly.`;
8061
+ return `${head} ${advice}, run: ${recoveryCommand}`;
8062
+ }
8063
+
7930
8064
  //#endregion
7931
8065
  //#region src/utils/bucket-region-client.ts
7932
8066
  /**
@@ -8013,6 +8147,23 @@ const LEGACY_KEY_DEPTH = 2;
8013
8147
  /** The `version: 2` region-prefixed key. */
8014
8148
  const NEW_KEY_DEPTH = 3;
8015
8149
  /**
8150
+ * Does a legacy record classified by {@link LegacyStateProbe} belong to an
8151
+ * operation targeting `region`?
8152
+ *
8153
+ * Free function, and shared by both consumers on purpose: the delete sweep
8154
+ * needs the probe's KIND as well as this verdict (to warn when a record may
8155
+ * have been left behind), and duplicating the mapping at that call site is
8156
+ * how the two would drift apart again.
8157
+ */
8158
+ function legacyProbeBelongsTo(probe, region) {
8159
+ switch (probe.kind) {
8160
+ case "region": return probe.region === region;
8161
+ case "no-region": return true;
8162
+ case "absent":
8163
+ case "unreadable": return false;
8164
+ }
8165
+ }
8166
+ /**
8016
8167
  * S3-based state backend using conditional writes for optimistic locking.
8017
8168
  *
8018
8169
  * State keys are region-scoped (`{prefix}/{stackName}/{region}/state.json`)
@@ -8178,7 +8329,7 @@ var S3StateBackend = class {
8178
8329
  await this.ensureClientForBucket();
8179
8330
  const newKey = this.getStateKey(stackName, region);
8180
8331
  if (await this.headObject(newKey)) return true;
8181
- return this.legacyMatchesRegion(stackName, region);
8332
+ return this.legacyBelongsToRegion(stackName, region);
8182
8333
  }
8183
8334
  /**
8184
8335
  * Get state for a stack, transparently falling back to the legacy key.
@@ -8310,7 +8461,12 @@ var S3StateBackend = class {
8310
8461
  ...await this.ownerParam(),
8311
8462
  Key: this.getStateKey(stackName, region)
8312
8463
  }));
8313
- if (await this.legacyMatchesRegion(stackName, region)) {
8464
+ const legacyProbe = await this.probeLegacyState(stackName);
8465
+ if (legacyProbe.kind === "unreadable") {
8466
+ const safeName = this.displayName(stackName);
8467
+ this.logger.warn(`Could not read the legacy state record for '${safeName}' while cleaning up (${legacyProbe.reason}). If one exists it was left in place. Re-run with --verbose for the details.`);
8468
+ }
8469
+ if (legacyProbeBelongsTo(legacyProbe, region)) {
8314
8470
  await this.s3Client.send(new DeleteObjectCommand({
8315
8471
  Bucket: this.config.bucket,
8316
8472
  ...await this.ownerParam(),
@@ -8330,6 +8486,48 @@ var S3StateBackend = class {
8330
8486
  }
8331
8487
  }
8332
8488
  /**
8489
+ * Delete a legacy state file that names no region (issue #2537).
8490
+ *
8491
+ * `deleteState` cannot serve this case: it takes a region, keys the primary
8492
+ * delete off it, and sweeps the legacy key only when that key's own `region`
8493
+ * field matches. A `version: 1` blob whose body carries no `region` at all
8494
+ * has nothing to match, so it falls through every branch there — which is
8495
+ * how `cdkd state orphan` came to report a removal it never performed.
8496
+ *
8497
+ * Unconditional, and deliberately so — but NOT because the caller's ref
8498
+ * proves the body names no region. `listStacks` derives that ref from
8499
+ * `readLegacyRegion`, which also returns undefined on a swallowed 403 / 503
8500
+ * / unparseable body, so a record that DOES name a region can surface
8501
+ * region-less. The delete is still right there: the caller
8502
+ * (`cdkd state orphan` with no `--stack-region`) means "every record for
8503
+ * this name", and this key is one of them. A caller that means something
8504
+ * narrower must not use this method.
8505
+ *
8506
+ * No rollback-journal sweep: journal keys exist only in the region-scoped
8507
+ * layout (see `getRollbackJournalKey`), so a region-less record can have
8508
+ * none.
8509
+ */
8510
+ async deleteLegacyState(stackName) {
8511
+ await this.ensureClientForBucket();
8512
+ const key = this.getLegacyStateKey(stackName);
8513
+ try {
8514
+ const safeStack = displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>";
8515
+ this.logger.debug(`Deleting legacy state: ${safeStack} (${displaySafe(key, { asciiOnly: true }) || "<unrenderable>"})`);
8516
+ await this.s3Client.send(new DeleteObjectCommand({
8517
+ Bucket: this.config.bucket,
8518
+ ...await this.ownerParam(),
8519
+ Key: key
8520
+ }));
8521
+ this.logger.debug(`Legacy state deleted: ${safeStack}`);
8522
+ } catch (error) {
8523
+ const normalized = normalizeAwsError(error, {
8524
+ bucket: this.config.bucket,
8525
+ operation: "DeleteObject"
8526
+ });
8527
+ throw new StateError(`Failed to delete legacy state for stack '${displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>"}': ${normalized.message}`, normalized);
8528
+ }
8529
+ }
8530
+ /**
8333
8531
  * List all stacks with state in the bucket.
8334
8532
  *
8335
8533
  * Returns one `{stackName, region}` pair per state file. Both layouts
@@ -8708,29 +8906,85 @@ var S3StateBackend = class {
8708
8906
  }
8709
8907
  }
8710
8908
  /**
8711
- * Read the legacy state's `region` field. Used for region matching during
8712
- * `stateExists` / `deleteState` and for assigning a region to legacy
8713
- * entries during `listStacks`.
8909
+ * Read the legacy key and classify what is there {@link LegacyStateProbe}
8910
+ * says what each answer means and which consumer acts on it.
8714
8911
  */
8715
- async readLegacyRegion(stackName) {
8912
+ /**
8913
+ * A stack name safe to put in a log line. Names reach this class from S3
8914
+ * key segments, which anyone able to write the bucket controls, so every
8915
+ * message that renders one goes through here (issue #2170's class).
8916
+ */
8917
+ displayName(stackName) {
8918
+ return displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>";
8919
+ }
8920
+ async probeLegacyState(stackName) {
8716
8921
  try {
8717
8922
  const response = await this.s3Client.send(new GetObjectCommand({
8718
8923
  Bucket: this.config.bucket,
8719
8924
  ...await this.ownerParam(),
8720
8925
  Key: this.getLegacyStateKey(stackName)
8721
8926
  }));
8722
- if (!response.Body) return void 0;
8927
+ if (!response.Body) {
8928
+ this.logger.debug(`Legacy state probe for '${this.displayName(stackName)}': response carried no body`);
8929
+ return {
8930
+ kind: "unreadable",
8931
+ reason: "the response carried no body"
8932
+ };
8933
+ }
8723
8934
  const bodyString = await response.Body.transformToString();
8724
- const state = JSON.parse(bodyString);
8725
- return typeof state.region === "string" ? state.region : void 0;
8935
+ const raw = JSON.parse(bodyString).region;
8936
+ if (!raw) return { kind: "no-region" };
8937
+ if (typeof raw !== "string") {
8938
+ this.logger.debug(`Legacy state probe for '${this.displayName(stackName)}': 'region' is ${typeof raw}, not a string`);
8939
+ return {
8940
+ kind: "unreadable",
8941
+ reason: `its 'region' field is ${typeof raw}, not a string`
8942
+ };
8943
+ }
8944
+ return {
8945
+ kind: "region",
8946
+ region: raw
8947
+ };
8726
8948
  } catch (error) {
8727
- if (isNoSuchKey(error)) return void 0;
8728
- this.logger.debug(`Could not read legacy state region for '${stackName}': ${error instanceof Error ? error.message : String(error)}`);
8729
- return;
8949
+ if (isNoSuchKey(error)) return { kind: "absent" };
8950
+ const { detail } = describeAwsFailure(error);
8951
+ this.logger.debug(`Could not read legacy state region for '${this.displayName(stackName)}': ${displaySafe(detail, { asciiOnly: true }) || "<unrenderable>"}`);
8952
+ return {
8953
+ kind: "unreadable",
8954
+ reason: displaySafe(error instanceof Error && error.name ? error.name : "an unknown error", { asciiOnly: true }) || "<unrenderable>"
8955
+ };
8730
8956
  }
8731
8957
  }
8732
- async legacyMatchesRegion(stackName, region) {
8733
- return await this.readLegacyRegion(stackName) === region;
8958
+ /**
8959
+ * The region a legacy record should be listed under, or `undefined` when it
8960
+ * names none / could not be read. Preserves `listStacks`'s behaviour across
8961
+ * the {@link probeLegacyState} split.
8962
+ */
8963
+ async readLegacyRegion(stackName) {
8964
+ const probe = await this.probeLegacyState(stackName);
8965
+ return probe.kind === "region" ? probe.region : void 0;
8966
+ }
8967
+ /**
8968
+ * Whether an operation targeting `region` owns the legacy record — the
8969
+ * DELETE-side counterpart of `tryGetLegacy`'s read gate, and issue #2550.
8970
+ *
8971
+ * The two must agree. `tryGetLegacy` accepts a body that names NO region
8972
+ * from any region (`if (state.region && state.region !== region)` is false
8973
+ * when the field is absent), so `cdkd destroy` reads such a record, deletes
8974
+ * the AWS resources, and finishes. The old equality test here answered
8975
+ * `undefined === 'us-east-1'` — false — so the record survived a successful
8976
+ * destroy, kept appearing in `cdkd state list`, and the next deploy of that
8977
+ * name planned updates against resources that were gone.
8978
+ *
8979
+ * `unreadable` is deliberately NOT treated as `no-region`: a 403, a 503 or a
8980
+ * malformed body says nothing about who owns the record, and a read that
8981
+ * failed must never authorise a delete. It collapsed into the same
8982
+ * `undefined` as the other two before, which is why the one-line fix — make
8983
+ * `undefined` match — was wrong: it would also have made `stateExists`
8984
+ * report state for a stack that has none, since `absent` reads the same way.
8985
+ */
8986
+ async legacyBelongsToRegion(stackName, region) {
8987
+ return legacyProbeBelongsTo(await this.probeLegacyState(stackName), region);
8734
8988
  }
8735
8989
  /**
8736
8990
  * Try to read the legacy `version: 1` state. Returns null when the legacy
@@ -9608,6 +9862,99 @@ var LockManager = class {
9608
9862
  /** Fixed marker substituted for a secret value in log / error output. */
9609
9863
  const SECRET_MASK = "***";
9610
9864
  /**
9865
+ * The UNCOLLAPSED companion of a {@link RecordedSecretValues} map: for each map
9866
+ * instance, every `expression -> plaintext` pair the resolver recorded INTO IT,
9867
+ * keyed by EXPRESSION (issue [#2485](https://github.com/go-to-k/cdkd/issues/2485)).
9868
+ *
9869
+ * WHY IT EXISTS. The map is keyed by PLAINTEXT, so two expressions resolving to
9870
+ * one value keep ONE entry — whichever the resolver recorded last. A WHOLE-token
9871
+ * leaf is immune (the position pass copies its own source), but a leaf that
9872
+ * EMBEDS a token in a literal string is redacted by the value scan, which can
9873
+ * only write the map's surviving expression: the versioned sibling's, for a
9874
+ * template that spells the un-versioned one, and the next deploy diffs that
9875
+ * leaf forever. Recovering the losing expression needs evidence the map has
9876
+ * discarded, and it has to be PASS-LOCAL: `recordedSecretExpressions` is
9877
+ * process-wide and says only that an expression IS secret, never what it
9878
+ * resolved to in THIS resource — so it cannot tell "the source token lost the
9879
+ * map slot to its sibling" from "the source token was never resolved here"
9880
+ * (a previous generation's bag, where writing today's expression over the
9881
+ * framed value would record something that was never deployed).
9882
+ *
9883
+ * Keyed by the map INSTANCE, so the evidence is exactly as pass-local as the
9884
+ * map itself: a map the resolver populated (the deploy's `perResourceSecrets`
9885
+ * entry, and equally the map drift / scrub / import hand their own resolution)
9886
+ * carries the pairs of THAT resolution, while a map the resolver did not
9887
+ * populate — a derived needle map, a nested-stack inheritance copy, a
9888
+ * `new Map(secrets)` copy — starts with no entries here and takes the
9889
+ * pre-#2485 fall-through, the safe direction. A copy loses the evidence
9890
+ * deliberately: a copy is not the pass that resolved anything.
9891
+ *
9892
+ * `CONFLICTING_PLAINTEXT` marks an expression this map saw resolve to TWO
9893
+ * values (a region-pinned re-resolution of one spelling, say); it then vouches
9894
+ * for nothing, which is the same "answer nothing you cannot prove" rule
9895
+ * {@link plaintextIndexOf} applies to the collapsed map's reverse index.
9896
+ */
9897
+ const resolvedPairsOf = /* @__PURE__ */ new WeakMap();
9898
+ /**
9899
+ * Record that `expression` resolved to `plaintext` in the pass that owns
9900
+ * `secrets` — the resolver's recording seam calls this beside its
9901
+ * `secrets.set(plaintext, expression)`, so the two never disagree about which
9902
+ * pass the evidence belongs to. Mask-only map entries (value `SECRET_MASK`)
9903
+ * never pass through that seam — they came from no `{{resolve:...}}` token —
9904
+ * so nothing here special-cases the mask string: a secret whose plaintext
9905
+ * happens to BE `***` is a secret like any other.
9906
+ */
9907
+ function recordResolvedPair(secrets, expression, plaintext) {
9908
+ let pairs = resolvedPairsOf.get(secrets);
9909
+ if (pairs === void 0) {
9910
+ pairs = /* @__PURE__ */ new Map();
9911
+ resolvedPairsOf.set(secrets, pairs);
9912
+ }
9913
+ const previous = pairs.get(expression);
9914
+ if (previous === void 0) pairs.set(expression, plaintext);
9915
+ else if (previous !== plaintext) pairs.set(expression, CONFLICTING_PLAINTEXT);
9916
+ }
9917
+ /**
9918
+ * Carry the resolved pairs of `from` into `to`, for the one copy of a
9919
+ * resolver-populated map that POSITIONS anything: the deploy engine accumulates
9920
+ * each stack's output resolution into its `outputSecrets` bag entry by entry,
9921
+ * and without this the copy would keep the collapsed entries while dropping the
9922
+ * evidence — so a literal `Output` embedding one of two same-plaintext
9923
+ * references would fall back to the value scan and persist the sibling's
9924
+ * expression. The engine's other entry-by-entry copy — an `Export.Name`'s
9925
+ * secrets into the pass map — deliberately does NOT call this: a name never
9926
+ * positions a leaf, a value re-using the same token records its own pair at
9927
+ * the seam, and the only thing the merge could add is a CONFLICT (a
9928
+ * non-cacheable `{{resolve:ssm:X}}` whose value moved between the value pass
9929
+ * and the name's resolution), which would destroy positioning the value pass
9930
+ * had earned. A pair that conflicts across the two maps is marked conflicting
9931
+ * in `to`, the same rule {@link recordResolvedPair} applies within one map.
9932
+ *
9933
+ * Deliberately NOT a general "copy the map" helper: every other new map is a
9934
+ * different PASS, and starting it without evidence is the safe direction.
9935
+ */
9936
+ function mergeResolvedPairs(from, to) {
9937
+ const pairs = resolvedPairsOf.get(from);
9938
+ if (pairs === void 0) return;
9939
+ for (const [expression, plaintext] of pairs) if (typeof plaintext === "string") recordResolvedPair(to, expression, plaintext);
9940
+ else {
9941
+ let target = resolvedPairsOf.get(to);
9942
+ if (target === void 0) {
9943
+ target = /* @__PURE__ */ new Map();
9944
+ resolvedPairsOf.set(to, target);
9945
+ }
9946
+ target.set(expression, CONFLICTING_PLAINTEXT);
9947
+ }
9948
+ }
9949
+ /**
9950
+ * The plaintext `expression` resolved to in the pass that owns `secrets`, or
9951
+ * `undefined` when that pass recorded nothing for it (or two different values).
9952
+ */
9953
+ function resolvedPlaintextOf(secrets, expression) {
9954
+ const recorded = resolvedPairsOf.get(secrets)?.get(expression);
9955
+ return typeof recorded === "string" ? recorded : void 0;
9956
+ }
9957
+ /**
9611
9958
  * Every `{{resolve:...}}` expression this process has PROVEN resolves to a
9612
9959
  * secret, as a SET — uncollapsed by resolved value (issue #1910).
9613
9960
  *
@@ -10618,8 +10965,9 @@ function isKnownSecretExpression(expression, secretExpressions) {
10618
10965
  return isSecretExpressionByVerdictOrSpelling(expression) || secretExpressions.has(expression);
10619
10966
  }
10620
10967
  /**
10621
- * The two arms of {@link isKnownSecretExpression} that need NO pass-local set:
10622
- * `secretsmanager` by SPELLING, and anything this process PROVED secret.
10968
+ * The arms of {@link isKnownSecretExpression} that need NO pass-local set:
10969
+ * `secretsmanager` / `ssm-secure` by SPELLING, and anything this process
10970
+ * PROVED secret.
10623
10971
  *
10624
10972
  * Split out so the resolver can ask the same question at the issue #2059
10625
10973
  * recording seam, where no `secretExpressions` set is in hand. It must not
@@ -10644,7 +10992,7 @@ function isKnownSecretExpression(expression, secretExpressions) {
10644
10992
  * change to a store this function only reads.
10645
10993
  */
10646
10994
  function isSecretExpressionByVerdictOrSpelling(expression) {
10647
- return expression.startsWith("{{resolve:secretsmanager:") || isRecordedSecretExpression(expression);
10995
+ return expression.startsWith("{{resolve:secretsmanager:") || expression.startsWith("{{resolve:ssm-secure:") || isRecordedSecretExpression(expression);
10648
10996
  }
10649
10997
  /**
10650
10998
  * The character class a `{{resolve:...}}` reference's INNER text is built from,
@@ -11188,6 +11536,124 @@ function positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions) {
11188
11536
  return matched;
11189
11537
  }
11190
11538
  /**
11539
+ * The ONE-span frame shared by {@link positionByEmbeddedSpan} and
11540
+ * {@link learnMixedLeafNeedle}: a source holding exactly one `{{resolve:...}}`
11541
+ * token, and a bag that starts with the source's prefix and ends with its
11542
+ * suffix with something non-empty between them that is NOT itself a complete
11543
+ * token (an already-redacted record is a persisted answer, not a plaintext).
11544
+ * `undefined` for any other shape. One helper rather than two copies so the
11545
+ * two refusals cannot drift apart.
11546
+ */
11547
+ function singleSpanFrame(bag, source) {
11548
+ const spans = dynamicReferenceSpans(source);
11549
+ if (spans.length !== 1) return void 0;
11550
+ const [span] = spans;
11551
+ const token = source.slice(span.start, span.end);
11552
+ const prefix = source.slice(0, span.start);
11553
+ const suffix = source.slice(span.end);
11554
+ if (bag.length <= prefix.length + suffix.length) return void 0;
11555
+ if (!bag.startsWith(prefix) || !bag.endsWith(suffix)) return void 0;
11556
+ const middle = bag.slice(prefix.length, bag.length - suffix.length);
11557
+ if (isSingleDynamicReferenceToken(middle)) return void 0;
11558
+ return {
11559
+ token,
11560
+ prefix,
11561
+ suffix,
11562
+ middle
11563
+ };
11564
+ }
11565
+ /**
11566
+ * Position a literal source leaf that EMBEDS exactly one `{{resolve:...}}`
11567
+ * token — `postgres://app-svc:{{resolve:ssm-secure:NAME}}@db/app` — by the
11568
+ * span the source states, writing `prefix + token + suffix` (issue
11569
+ * [#2485](https://github.com/go-to-k/cdkd/issues/2485)).
11570
+ *
11571
+ * WHY THE VALUE SCAN IS NOT ENOUGH HERE. The scan writes the map's surviving
11572
+ * expression for a plaintext, and the map keeps one expression per plaintext:
11573
+ * a whole-value `NAME:1` sibling that resolved LAST leaves `NAME:1` as the only
11574
+ * expression for the value, so the embedded leaf persists the versioned
11575
+ * spelling for a template that spells `NAME`, and the deploy diff — expression
11576
+ * against expression — reports that leaf on every run. The whole-token arm of
11577
+ * {@link redactByPath} is immune because it copies its own source; this arm
11578
+ * gives the one-span literal leaf the same immunity.
11579
+ *
11580
+ * THE EVIDENCE, and why the shape of the frame is not enough on its own: the
11581
+ * frame check (`bag` starts with the source's prefix and ends with its suffix,
11582
+ * with something between) is what {@link learnMixedLeafNeedle} already uses to
11583
+ * LEARN a needle, and it proves only that the bag has the source's shape. The
11584
+ * bag can also be a PREVIOUS generation's (`cdkd scrub`, a state-sourced walk)
11585
+ * with an earlier plaintext framed exactly like this, and writing today's
11586
+ * token over it would record an expression that was never deployed at that
11587
+ * position — the hazard `sourceIsSameGeneration` exists for on the whole-token
11588
+ * arm. So the middle must EQUAL what THIS pass recorded the source token
11589
+ * resolving to ({@link recordResolvedPair}, per map instance): that is evidence
11590
+ * of this resolution, not of shape, and it is absent by construction for every
11591
+ * bag this pass did not produce. It is also what keeps a PUBLIC `ssm` token
11592
+ * resolved (issue #1901) — the resolver records only secret verdicts — and what
11593
+ * keeps a mask-only `NoEcho` value out (never recorded).
11594
+ *
11595
+ * WHAT THIS EVIDENCE DOES NOT CLAIM, stated because a reviewer asked: it does
11596
+ * not prove the bag was produced FROM this source. A previous generation's bag
11597
+ * whose framed middle happens to EQUAL a plaintext this pass resolved the
11598
+ * source token to (`cdkd scrub` walking an old record against today's template,
11599
+ * or a failed deploy persisting an old bag) takes this arm and persists TODAY's
11600
+ * expression at that position. That is not a new claim: the value scan the
11601
+ * arm replaces rewrites that same plaintext onto one of THIS pass's expressions
11602
+ * regardless of generation — the map holds no other — so the class of answer
11603
+ * is unchanged and only the choice within it improves (the source's own
11604
+ * token rather than the map's survivor). The generation hazard this arm must
11605
+ * not create is the whole-token arm's: a middle that is ALREADY an expression
11606
+ * (a persisted answer from another generation), which the token refusal below
11607
+ * keeps out — and, by the same argument, any leaf the value scan would NOT
11608
+ * rewrite to exactly `prefix + survivor + suffix`: a middle shorter than the
11609
+ * scan's needle floor (an embedded 1-3 character secret stays the scan's
11610
+ * documented residual — issue #2516 tracks closing it with a bound that
11611
+ * proves the bag's generation, which this evidence does not), a whole leaf
11612
+ * that is itself another recorded plaintext, a needle starting in the prefix
11613
+ * and overlapping the middle. The
11614
+ * arm checks that equivalence against the scan's own answer rather than
11615
+ * re-deriving the scan's rules. Pinned by the cross-generation cases in
11616
+ * `secret-redaction-embedded-span.test.ts`.
11617
+ *
11618
+ * One shape reaches this arm that a reader may not expect: a WHOLE-token
11619
+ * source that FAILED the whole-token arm's `isKnownSecretExpression` gate (an
11620
+ * `ssm` token whose type came back unclassifiable and which lost the map slot
11621
+ * to a sibling). Its "frame" is empty, and if this pass recorded it resolving
11622
+ * to the bag it is written back as itself — an expression, and the leaf's own,
11623
+ * where the scan wrote the survivor. Stated so it is not mistaken for a leak.
11624
+ *
11625
+ * Everything else keeps the pre-#2485 fall-through: two or more spans (which
11626
+ * span produced which value is genuinely ambiguous when they share one), an
11627
+ * `Fn::Sub` / `Fn::Join` source (an object, not this arm at all — issue #2320's
11628
+ * placeholder primitive), a frame mismatch, a middle that is itself a complete
11629
+ * token (an already-redacted record, per the same refusal
11630
+ * {@link learnMixedLeafNeedle} makes), and a middle this pass cannot vouch for.
11631
+ *
11632
+ * The frame is copied from the SOURCE, not scanned. A needle occurring in the
11633
+ * literal frame would be a reference the template never had at that offset —
11634
+ * the fabricated-baseline direction {@link preferPositionDecisions} refuses —
11635
+ * and the whole-token arm returns its source unscanned for the same reason.
11636
+ *
11637
+ * RETURNS THE VALUE SCAN'S ANSWER ON EVERY REFUSAL, not `undefined`: the scan
11638
+ * is computed once, here, for `(bag, secrets)` — the arm's bound below compares
11639
+ * against it, and every fall-through IS it — so the compared value provably
11640
+ * comes from the same bag and map the arm positions. An earlier revision took
11641
+ * the scan as a parameter, which left the bound one wrong caller away from
11642
+ * comparing against a scan of some other bag with no type error.
11643
+ */
11644
+ function positionByEmbeddedSpan(bag, source, secrets) {
11645
+ const scanned = redactSecretsForState(bag, secrets);
11646
+ const frame = singleSpanFrame(bag, source);
11647
+ if (frame === void 0) return scanned;
11648
+ const { token, prefix, suffix, middle } = frame;
11649
+ const recorded = resolvedPlaintextOf(secrets, token);
11650
+ if (recorded === void 0 || recorded !== middle) return scanned;
11651
+ const survivor = secrets.get(middle);
11652
+ if (survivor === void 0) return scanned;
11653
+ if (scanned !== prefix + survivor + suffix) return scanned;
11654
+ return prefix + token + suffix;
11655
+ }
11656
+ /**
11191
11657
  * Keys tried, in order, when pairing two arrays whose ORDER cannot be trusted
11192
11658
  * (issue #1915).
11193
11659
  *
@@ -11387,7 +11853,7 @@ function redactByPath(bag, source, secrets, rules, secretExpressions) {
11387
11853
  if (!rules.sourceIsSameGeneration && isSingleDynamicReferenceToken(bag)) return secrets.get(bag) ?? bag;
11388
11854
  return source;
11389
11855
  }
11390
- return redactSecretsForState(bag, secrets);
11856
+ return positionByEmbeddedSpan(bag, source, secrets);
11391
11857
  }
11392
11858
  if (typeof bag === "string" && isPlainObject$2(source)) {
11393
11859
  const certified = positionByCrossStackSource(bag, source, secrets);
@@ -11505,12 +11971,13 @@ function subtreeHasDynamicReference(value) {
11505
11971
  * Widening it changes one answer, in the SAFE direction for BOTH readers.
11506
11972
  *
11507
11973
  * `drift.ts`'s `survivingDynamicReferences` is the reader that is easy to
11508
- * forget, because it lives in another file — it feeds `isSecretBySpelling`,
11509
- * so seeing MORE tokens can only mask more, never less. Do not shorten this
11510
- * to "the only reader": that sentence is what a later editor uses to bound
11511
- * the blast radius of touching the class, and getting it wrong points them
11512
- * away from the report / `--json` / `--accept` path where an unmasked
11513
- * `ssm-secure` survivor would surface.
11974
+ * forget, because it lives in another file — it feeds the survivor REPORT
11975
+ * (`onUnresolved`, and through it the `unresolvedToken` cause), so seeing MORE
11976
+ * tokens can only report more, never less. Do not shorten this to "the only
11977
+ * reader": that sentence is what a later editor uses to bound the blast
11978
+ * radius of touching the class, and getting it wrong points them away from
11979
+ * the report / `--json` / `--accept` path where an unreported survivor would
11980
+ * surface.
11514
11981
  *
11515
11982
  * The other reader is the DECLARED direction for issue #1901:
11516
11983
  * {@link mixedLeafMayCarryPublicReference}, which asks whether a MIXED leaf
@@ -11916,9 +12383,9 @@ function unkeyedArrayPairsByAnchors(bag, source) {
11916
12383
  * from one the pass decided IN FAVOUR of the value already there. Two shapes
11917
12384
  * hit it, both fabricating a baseline `cdkd drift --revert` then pushes:
11918
12385
  *
11919
- * - the resolver's unsupported-service arm leaves an `{{resolve:ssm-secure:`
11920
- * token LITERAL, so AWS echoes it back and the source leaf EQUALS the bag
11921
- * leaf. The string arm returns `source` — a decision — and the equality made
12386
+ * - the resolver's unsupported-service arm leaves a `{{resolve:...}}` token it
12387
+ * has no arm for LITERAL (`ssm-secure:` was one until issue #2482), so AWS
12388
+ * echoes it back and the source leaf EQUALS the bag leaf. The string arm returns `source` — a decision — and the equality made
11922
12389
  * it look like no decision at all. (A BARE such token takes the whole-token
11923
12390
  * arm and one embedded in text takes the mixed-leaf arm; both decide, and
11924
12391
  * both were misread.)
@@ -12102,7 +12569,8 @@ function learnWholeTokenNeedle(collector, bag, source) {
12102
12569
  * would then contain a whole `{{resolve:...}}` token and a resolved readback
12103
12570
  * cannot end with one. The shape it genuinely decides is a second reference
12104
12571
  * that survives LITERALLY in the readback — the resolver's
12105
- * unsupported-service arm (`ssm-secure:`) produces exactly that — where the
12572
+ * unsupported-service arm produces exactly that (`ssm-secure:` did until
12573
+ * issue #2482; a spelling with no arm still does) — where the
12106
12574
  * extraction would in fact be right and is declined anyway. Measured: a
12107
12575
  * both-resolved fixture leaves this line unfenced.
12108
12576
  * - the source's literal PREFIX and SUFFIX must both be present at the ends of
@@ -12116,17 +12584,10 @@ function learnWholeTokenNeedle(collector, bag, source) {
12116
12584
  * correctly, while an `indexOf` scan would cut it short.
12117
12585
  */
12118
12586
  function learnMixedLeafNeedle(collector, bag, source) {
12119
- const spans = dynamicReferenceSpans(source);
12120
- if (spans.length !== 1) return;
12121
- const [span] = spans;
12122
- const token = source.slice(span.start, span.end);
12587
+ const frame = singleSpanFrame(bag, source);
12588
+ if (frame === void 0) return;
12589
+ const { token, middle: plaintext } = frame;
12123
12590
  if (!expressionMaySeedANeedle(token)) return;
12124
- const prefix = source.slice(0, span.start);
12125
- const suffix = source.slice(span.end);
12126
- if (bag.length <= prefix.length + suffix.length) return;
12127
- if (!bag.startsWith(prefix) || !bag.endsWith(suffix)) return;
12128
- const plaintext = bag.slice(prefix.length, bag.length - suffix.length);
12129
- if (isSingleDynamicReferenceToken(plaintext)) return;
12130
12591
  learnNeedle(collector, plaintext, token);
12131
12592
  }
12132
12593
  /**
@@ -15382,17 +15843,22 @@ function s3BucketWebsiteUrl(bucketName, region) {
15382
15843
  /**
15383
15844
  * The `{{resolve:<service>:...}}` families whose value can be a SECRET, and
15384
15845
  * therefore the only ones the region question below is asked about: every
15385
- * `secretsmanager` reference by spelling, and every `ssm` one, which is secret
15386
- * exactly when its parameter is a `SecureString` (issue #1901).
15387
- *
15388
- * Every OTHER service is `local` because cdkd cannot resolve it at all, NOT
15389
- * because it is public. `ssm-secure` is the live example and is emphatically
15390
- * not public: `resolveDynamicReferences` has no arm for it, so the literal
15391
- * token is passed through to AWS and CloudFormation resolves it SERVER-side.
15392
- * cdkd never holds its value, so there is no region for cdkd to get wrong
15393
- * which is the only reason it can be waved through here.
15394
- */
15395
- const REPLAY_SECRET_SERVICES = /* @__PURE__ */ new Set(["secretsmanager", "ssm"]);
15846
+ * `secretsmanager` reference by spelling, every `ssm-secure` one by spelling
15847
+ * (issue #2482 it is resolved through the same `GetParameter` as `ssm`, so
15848
+ * the wrong region answers it in exactly the same way), and every `ssm` one,
15849
+ * which is secret exactly when its parameter is a `SecureString` (issue #1901).
15850
+ *
15851
+ * Every OTHER service is `local` because cdkd cannot resolve it at all — the
15852
+ * resolver's unsupported-service arm leaves such a token in place, so there
15853
+ * is no lookup for a region to get wrong. None of CloudFormation's three
15854
+ * services is in that position any more; the arm exists for a spelling that
15855
+ * is not a dynamic reference at all, or one AWS adds later.
15856
+ */
15857
+ const REPLAY_SECRET_SERVICES = /* @__PURE__ */ new Set([
15858
+ "secretsmanager",
15859
+ "ssm",
15860
+ "ssm-secure"
15861
+ ]);
15396
15862
  /**
15397
15863
  * Split a `{{resolve:secretsmanager:...}}` inner body into its SECRET_ID.
15398
15864
  *
@@ -15428,7 +15894,7 @@ function secretsManagerSecretId(inner) {
15428
15894
  * a different thing in the refusal message than the one that would be read.
15429
15895
  */
15430
15896
  function ssmParameterName(inner) {
15431
- return inner.substring(4);
15897
+ return inner.substring(inner.indexOf(":") + 1);
15432
15898
  }
15433
15899
  /**
15434
15900
  * The region an ARN names, or `undefined` for anything that is not an ARN with
@@ -16155,7 +16621,7 @@ function normalizeAwsTagsToCfn(tags) {
16155
16621
  * through `update()` and surface as a hard AWS rejection, so we sanitize
16156
16622
  * the wire-layer payload while keeping the read-side placeholder
16157
16623
  * intact. This is the Class 2 pattern from
16158
- * `docs/provider-development.md § 3b`.
16624
+ * `docs/provider-rules.md#readcurrentstate-for-drift-detection`.
16159
16625
  */
16160
16626
  function sanitizeDescription(value) {
16161
16627
  if (value === void 0 || value === null) return void 0;
@@ -20488,7 +20954,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
20488
20954
  });
20489
20955
  for (const { fullMatch, inner } of matches) {
20490
20956
  const service = inner.split(":")[0];
20491
- const isKnownSecret = service === "secretsmanager" || recordedSecretExpressions.has(fullMatch);
20957
+ const isKnownSecret = service === "secretsmanager" || service === "ssm-secure" || recordedSecretExpressions.has(fullMatch);
20492
20958
  if (isKnownSecret && context?.skipDynamicReferences) continue;
20493
20959
  const regionVerdict = classifyReplaySecretRegion(fullMatch, this.explicitRegion ?? this.resolverRegion, context?.producerRegions);
20494
20960
  if (regionVerdict.kind === "ambiguous") throw markNonRetryable(new DynamicReferenceRegionAmbiguousError(`Refusing to resolve the secret reference ${fullMatch}: it names '${regionVerdict.secretName}' without a region, and this stack reads from ${regionVerdict.foreignProducerRegions.join(", ")} as well as its own region. cdkd cannot tell which one must answer, and resolving against the wrong one yields a different secret. Spell the reference as a full ARN to say which region owns it.`));
@@ -20499,7 +20965,10 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
20499
20965
  }
20500
20966
  const cached = this.cachedDynamicReferences.get(fullMatch);
20501
20967
  if (cached) {
20502
- if (cached.secret && cached.value) context?.recordedSecretValues?.set(cached.value, fullMatch);
20968
+ if (cached.secret && cached.value) {
20969
+ context?.recordedSecretValues?.set(cached.value, fullMatch);
20970
+ if (context?.recordedSecretValues) recordResolvedPair(context.recordedSecretValues, fullMatch, cached.value);
20971
+ }
20503
20972
  result = result.replace(fullMatch, () => cached.value);
20504
20973
  continue;
20505
20974
  }
@@ -20531,6 +21000,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
20531
21000
  if (!decrypt) continue;
20532
21001
  }
20533
21002
  resolved = param.value;
21003
+ } else if (service === "ssm-secure") {
21004
+ const param = await this.resolveSSMReference(parts, true, "ssm-secure");
21005
+ if (!param.secure) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Refusing to resolve ${fullMatch}: the parameter is a ${param.type} parameter, and the ssm-secure spelling is defined for SecureString parameters only. Reference it as {{resolve:ssm:...}} if it is public configuration.`));
21006
+ isSecret = true;
21007
+ resolved = param.value;
20534
21008
  } else {
20535
21009
  this.logger.warn(`Unsupported dynamic reference service: ${service}`);
20536
21010
  continue;
@@ -20541,7 +21015,8 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
20541
21015
  });
20542
21016
  if (isSecret && resolved) {
20543
21017
  context?.recordedSecretValues?.set(resolved, fullMatch);
20544
- if (service === "secretsmanager") this.pinSecretVerdict(fullMatch, true);
21018
+ if (context?.recordedSecretValues) recordResolvedPair(context.recordedSecretValues, fullMatch, resolved);
21019
+ if (service === "secretsmanager" || service === "ssm-secure") this.pinSecretVerdict(fullMatch, true);
20545
21020
  }
20546
21021
  result = result.replace(fullMatch, () => resolved);
20547
21022
  }
@@ -20751,16 +21226,16 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
20751
21226
  * discard the value when `secure` is set — it is ciphertext, not the resolved
20752
21227
  * reference.
20753
21228
  */
20754
- async resolveSSMReference(parts, decrypt = true) {
21229
+ async resolveSSMReference(parts, decrypt = true, service = "ssm") {
20755
21230
  const parameterName = parts.slice(1).join(":");
20756
- if (!parameterName) throw new Error("Dynamic reference: ssm PARAMETER_NAME is required");
20757
- this.logger.debug(`Resolving dynamic reference: ssm:${parameterName}`);
21231
+ if (!parameterName) throw new Error(`Dynamic reference: ${service} PARAMETER_NAME is required`);
21232
+ this.logger.debug(`Resolving dynamic reference: ${service}:${parameterName}`);
20758
21233
  const client = this.clientsForRegion(this.explicitRegion).ssm;
20759
21234
  const command = new GetParameterCommand({
20760
21235
  Name: parameterName,
20761
21236
  WithDecryption: decrypt
20762
21237
  });
20763
- const response = await this.sendWithThrottleRetry(() => client.send(command), `ssm:${parameterName}`);
21238
+ const response = await this.sendWithThrottleRetry(() => client.send(command), `${service}:${parameterName}`);
20764
21239
  const paramValue = response.Parameter?.Value;
20765
21240
  if (paramValue === void 0 || paramValue === null) throw new Error(`Dynamic reference: SSM parameter '${parameterName}' not found or has no value`);
20766
21241
  const paramType = response.Parameter?.Type;
@@ -20768,7 +21243,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
20768
21243
  if (secure && paramType !== "SecureString" && !this.warnedUnrecognizedSsmTypes.has(`${parameterName}\u0000${String(paramType)}`)) {
20769
21244
  this.warnedUnrecognizedSsmTypes.add(`${parameterName}\u0000${String(paramType)}`);
20770
21245
  const reported = paramType === void 0 ? "(absent)" : `'${String(paramType)}'`;
20771
- this.logger.warn(`SSM parameter '${parameterName}' reported an unrecognized Type ${reported} — treating its value as a secret, so cdkd will persist the {{resolve:ssm:...}} expression rather than the resolved value. Declare the parameter as String / StringList if it is public config.`);
21246
+ this.logger.warn(`SSM parameter '${parameterName}' reported an unrecognized Type ${reported} — treating its value as a secret, so cdkd will persist the {{resolve:${service}:...}} expression rather than the resolved value. Declare the parameter as String / StringList if it is public config.`);
20772
21247
  }
20773
21248
  return {
20774
21249
  value: paramValue,
@@ -21903,7 +22378,7 @@ var CloudControlProvider = class {
21903
22378
  const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
21904
22379
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
21905
22380
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
21906
- const { ASGProvider } = await import("./asg-provider-DcFgnQQO.js").then((n) => n.n);
22381
+ const { ASGProvider } = await import("./asg-provider-CKEf903K.js").then((n) => n.n);
21907
22382
  return withIndeterminateGuard(await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
21908
22383
  }
21909
22384
  const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
@@ -23088,42 +23563,6 @@ function interruptWatchListenerCount() {
23088
23563
  return sharedSigintHandler === void 0 ? 0 : 1;
23089
23564
  }
23090
23565
 
23091
- //#endregion
23092
- //#region src/state/state-prefix.ts
23093
- /**
23094
- * The default S3 key prefix for cdkd state.
23095
- *
23096
- * Homed in the STATE layer rather than in `src/cli/commands/state-file-keys.ts`
23097
- * (which re-exports it, so its four existing importers are unchanged) because
23098
- * `src/state/lock-contention-message.ts` needs it to decide whether a recovery
23099
- * hint should spell `--state-prefix` at all, and a `src/state/**` module
23100
- * importing from `src/cli/commands/**` inverts the layering — the CLI sits
23101
- * ABOVE the state layer in the 7-layer architecture, not below it.
23102
- *
23103
- * Note this is only the DEFAULT. Other commands accept `--state-prefix`, so
23104
- * whole-bucket listings deliberately do not scope to it.
23105
- */
23106
- const DEFAULT_STATE_PREFIX = "cdkd";
23107
- /**
23108
- * The state-bucket prefix `CustomResourceProvider` PUTs its response
23109
- * placeholders under, one object per invocation
23110
- * (`custom-resource-responses/{requestId}.json`).
23111
- *
23112
- * Homed here for the same layering reason as {@link DEFAULT_STATE_PREFIX}: the
23113
- * PRODUCER is `src/provisioning/providers/custom-resource-provider.ts` and the
23114
- * COLLECTOR is `src/cli/commands/gc.ts`, so a copy in either would be a copy
23115
- * the other could drift from — and the two spellings would then disagree about
23116
- * which objects exist, which is the only way a sweeper can miss the family it
23117
- * was written for (issue #2052). `src/cli/commands/state-file-keys.ts`
23118
- * re-exports it so gc reads it alongside the other state-key constants.
23119
- *
23120
- * Note this is only the DEFAULT: `ProviderRegistry` can be configured with a
23121
- * different `responsePrefix`, so a sweep scoped to this value is a sweep of the
23122
- * default layout. gc has no access to a non-default one — nothing persists it —
23123
- * which is stated at the sweep's own call site rather than implied here.
23124
- */
23125
- const CUSTOM_RESOURCE_RESPONSE_PREFIX = "custom-resource-responses";
23126
-
23127
23566
  //#endregion
23128
23567
  //#region src/provisioning/masked-retry-logger.ts
23129
23568
  /**
@@ -27724,7 +28163,7 @@ function isCustomResource(resourceType) {
27724
28163
  * live value on removal. Route every optional mutable field of a
27725
28164
  * merge-semantics API through this helper, with `clearValue` set to the
27726
28165
  * property's CFn default or the SDK-documented clear sentinel — see
27727
- * docs/provider-development.md §2a for the per-field checklist.
28166
+ * docs/provider-rules.md#update-removal-semantics-clear-on-removal for the per-field checklist.
27728
28167
  *
27729
28168
  * Returns `newValue` when present, the `clearValue` when the field was
27730
28169
  * present before and is now absent (removal), and `undefined` when it was
@@ -28741,9 +29180,16 @@ function formatResourceLine(op, logicalId, resourceType, verbOverride) {
28741
29180
  *
28742
29181
  * To avoid an accidental data-loss footgun, cdkd refuses to recreate
28743
29182
  * any resource whose type is in {@link STATEFUL_TYPES} unless the user
28744
- * ALSO passes `--force-stateful-recreation`. The two-flag protection
28745
- * mirrors `--remove-protection`'s pattern (see
28746
- * `src/cli/commands/destroy-runner.ts`).
29183
+ * ALSO passes `--force-stateful-recreation`. On the `--recreate-via-*`
29184
+ * and `--replace` opt-ins that is a two-flag protection, mirroring
29185
+ * `--remove-protection`'s pattern (see
29186
+ * `src/cli/commands/destroy-runner.ts`). It is NOT only that shape any
29187
+ * more: the guard also runs on replacement paths a plain `cdkd deploy`
29188
+ * reaches with no flag at all (a property-driven replacement, and the
29189
+ * update-failure fallback's Cloud Control trigger — issue [#2514]),
29190
+ * where `--force-stateful-recreation` is the ONLY flag involved. See
29191
+ * {@link isStatefulRecreateTargetForReplace} for the mid-deploy variant
29192
+ * those paths use.
28747
29193
  *
28748
29194
  * The list is hand-curated and intentionally **conservative**: every
28749
29195
  * type here carries user data that the AWS service does NOT
@@ -28751,18 +29197,59 @@ function formatResourceLine(op, logicalId, resourceType, verbOverride) {
28751
29197
  * AWS service treats as ephemeral (e.g. Lambda Function, IAM Role)
28752
29198
  * are NOT in this list — recreate is cheap.
28753
29199
  *
28754
- * Two entries are **conditionally stateful** they only count when
28755
- * the resource actually contains data:
29200
+ * Two entries carry a CONDITION instead of counting unconditionally.
29201
+ * The condition is what the guard evaluates; failing it is not a
29202
+ * finding that the resource holds no data (see the LogGroup note):
28756
29203
  *
28757
29204
  * - `AWS::S3::Bucket`: empty buckets are safe to recreate. The
28758
- * deploy engine probes `s3:ListObjectsV2` at plan time and only
29205
+ * deploy engine probes `s3:ListObjectVersions` at plan time and only
28759
29206
  * refuses when the bucket has at least one object.
28760
- * - `AWS::Logs::LogGroup`: a log group with `RetentionInDays`
28761
- * undefined or zero is functionally ephemeral. The deploy engine
28762
- * refuses only when `RetentionInDays > 0`.
29207
+ * - `AWS::Logs::LogGroup`: the deploy engine refuses only when
29208
+ * `RetentionInDays > 0`. NOTE this is a KNOWN GAP, not a statement
29209
+ * that the rest are empty: an unset or zero retention is CloudWatch
29210
+ * Logs' "never expire", the most data-bearing setting there is, and
29211
+ * `LogsLogGroupProvider` writes `0` for precisely that. Issue
29212
+ * [#2558] tracks it; the predicate is left as-is here.
28763
29213
  *
28764
29214
  * Both conditional checks live in {@link isStatefulRecreateTarget};
28765
29215
  * the bare {@link STATEFUL_TYPES} set is the type-only first-cut.
29216
+ *
29217
+ * **Lower bound, enforced by a test** (issue [#2514]'s review round):
29218
+ * every type in `final-snapshot.ts`'s `ATOMIC_FINAL_SNAPSHOT_TYPES` ∪
29219
+ * `PRE_DELETE_SNAPSHOT_TYPES` must appear here. That union is the
29220
+ * CloudFormation-documented `DeletionPolicy: Snapshot`-capable list, and
29221
+ * CloudFormation permits the attribute exactly where deleting the resource
29222
+ * destroys data worth capturing first — so membership there is an
29223
+ * AWS-authored statement that the type is data-bearing. The types that were
29224
+ * in that union and NOT here (`AWS::Redshift::Cluster`,
29225
+ * `AWS::ElastiCache::ReplicationGroup`, `AWS::ElastiCache::CacheCluster`)
29226
+ * meant cdkd took a final snapshot before a `cdkd destroy` of them
29227
+ * while replacing them mid-deploy with no consent flag at all.
29228
+ * `tests/unit/provisioning/stateful-types.test.ts` pins the subset
29229
+ * relation so a future addition to either snapshot set cannot land without
29230
+ * the guard entry.
29231
+ *
29232
+ * The relation is deliberately ONE-directional: most data-bearing types
29233
+ * (S3, DynamoDB, LogGroup, ECR, …) have no snapshot API at all and
29234
+ * CloudFormation rejects `DeletionPolicy: Snapshot` on them, so this set is
29235
+ * a strict superset and the reverse containment would be wrong.
29236
+ *
29237
+ * **Second lower bound, also enforced by a test**: every resource type
29238
+ * registered to a provider whose `delete()` consults
29239
+ * `DeleteContext.forceDataDelete` must appear here. That field is set ONLY
29240
+ * by the replacement / recreate delete sites under
29241
+ * `--force-stateful-recreation`, so a provider reading it has already
29242
+ * declared its delete destroys user data — which makes the guard list's
29243
+ * agreement checkable rather than hand-curated.
29244
+ * `AWS::S3Express::DirectoryBucket` was the one type on the wrong side.
29245
+ *
29246
+ * Neither fence can see a provider whose delete destroys data with NO opt-in
29247
+ * at all — an unconditional empty (`S3TablesProvider.deleteTableBucket`,
29248
+ * `S3VectorsProvider.deleteVectorBucket`) or a plainly destructive API call
29249
+ * (`KMSProvider`'s `ScheduleKeyDeletion`,
29250
+ * `CodeCommitRepositoryProvider`'s `DeleteRepository`). Those types are
29251
+ * hand-added with the reason at the entry, and a provider added that way
29252
+ * should be reviewed for whether it wants a `forceDataDelete` gate too.
28766
29253
  */
28767
29254
  const STATEFUL_TYPES = /* @__PURE__ */ new Set([
28768
29255
  "AWS::RDS::DBInstance",
@@ -28773,17 +29260,29 @@ const STATEFUL_TYPES = /* @__PURE__ */ new Set([
28773
29260
  "AWS::Neptune::DBCluster",
28774
29261
  "AWS::DynamoDB::Table",
28775
29262
  "AWS::DynamoDB::GlobalTable",
29263
+ "AWS::Redshift::Cluster",
29264
+ "AWS::ElastiCache::CacheCluster",
29265
+ "AWS::ElastiCache::ReplicationGroup",
28776
29266
  "AWS::EFS::FileSystem",
28777
29267
  "AWS::FSx::FileSystem",
28778
29268
  "AWS::S3::Bucket",
29269
+ "AWS::S3Express::DirectoryBucket",
29270
+ "AWS::S3Tables::TableBucket",
29271
+ "AWS::S3Tables::Table",
29272
+ "AWS::S3Tables::Namespace",
29273
+ "AWS::S3Vectors::VectorBucket",
28779
29274
  "AWS::ECR::Repository",
28780
29275
  "AWS::EC2::Volume",
29276
+ "AWS::EMR::Cluster",
28781
29277
  "AWS::Kinesis::Stream",
28782
29278
  "AWS::Elasticsearch::Domain",
28783
29279
  "AWS::OpenSearchService::Domain",
28784
29280
  "AWS::Cognito::UserPool",
28785
29281
  "AWS::SecretsManager::Secret",
28786
29282
  "AWS::SSM::Parameter",
29283
+ "AWS::KMS::Key",
29284
+ "AWS::KMS::ReplicaKey",
29285
+ "AWS::CodeCommit::Repository",
28787
29286
  "AWS::Glue::Database",
28788
29287
  "AWS::Glue::Table",
28789
29288
  "AWS::Logs::LogGroup",
@@ -28804,7 +29303,7 @@ const STATEFUL_TYPES = /* @__PURE__ */ new Set([
28804
29303
  const MULTI_REGION_RECREATE_BLOCKED_TYPES = /* @__PURE__ */ new Set(["AWS::DynamoDB::GlobalTable"]);
28805
29304
  /**
28806
29305
  * Cheap, synchronous read of the resource's recorded properties only.
28807
- * For `AWS::S3::Bucket` this returns `null` — the live `ListObjectsV2`
29306
+ * For `AWS::S3::Bucket` this returns `null` — the live `ListObjectVersions`
28808
29307
  * probe to distinguish empty buckets (safe to recreate) from
28809
29308
  * non-empty (data loss) lives in
28810
29309
  * `src/deployment/recreate-targets.ts#probeStatefulRecreateTargetsAsync`
@@ -28827,17 +29326,26 @@ function isStatefulRecreateTargetSync(resourceType, recordedProperties) {
28827
29326
  return "always";
28828
29327
  }
28829
29328
  /**
28830
- * Conservative variant for the `cdkd deploy --replace` mid-deploy guard.
29329
+ * Conservative variant for the deploy engine's mid-deploy guard sites, which
29330
+ * between them serve the paths below — only one of which is reached by a flag:
28831
29331
  *
28832
- * `--replace` catches a provider's immutable-update rejection while the deploy
28833
- * is already in flight, so unlike the `--recreate-via-*` pre-flight, which
29332
+ * - property-driven replacement (an immutable / createOnly property changed
29333
+ * in the template) fires on a plain `cdkd deploy`, no flag;
29334
+ * - the update-failure fallback's Cloud Control trigger (an
29335
+ * `UnsupportedActionException` / "does not support UPDATE" rejection) —
29336
+ * also no flag (issue [#2514]);
29337
+ * - the same fallback's `--replace` trigger (an SDK provider's typed
29338
+ * `ResourceUpdateNotSupportedError`).
29339
+ *
29340
+ * All of them catch the rejection or classify the diff while the deploy is
29341
+ * already in flight, so — unlike the `--recreate-via-*` pre-flight, which
28834
29342
  * runs {@link probeStatefulRecreateTargetsAsync} (`s3:ListObjectVersions`) —
28835
29343
  * there is no opportunity to probe an `AWS::S3::Bucket`'s object count. The
28836
29344
  * sync check returns `null` for S3 (it defers to that async probe), which would
28837
29345
  * let a NON-EMPTY bucket be DELETE + CREATEd (data loss) without
28838
29346
  * `--force-stateful-recreation`. To stay fail-safe, treat a deferred S3 bucket
28839
29347
  * as stateful here: the user must pass `--force-stateful-recreation` to replace
28840
- * ANY S3 bucket via `--replace`, empty or not. Every other type matches
29348
+ * ANY S3 bucket on any of those paths, empty or not. Every other type matches
28841
29349
  * {@link isStatefulRecreateTargetSync} exactly (the LogGroup retention check is
28842
29350
  * fully resolvable from recorded properties, so no conservatism is needed there).
28843
29351
  */
@@ -30818,7 +31326,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
30818
31326
  refuseMaskedReplayBaseline(resolvedPrevProps, op.logicalId);
30819
31327
  recordNestedStackParameterExpressions(secrets, op.resourceType, resolvedPrevProps, prev.properties, STATE_DERIVED_RULES);
30820
31328
  logger.info(` Rollback: Reversing replacement of ${op.logicalId} (${op.resourceType}) — re-creating the old resource and deleting the new one`);
30821
- if (STATEFUL_TYPES.has(op.resourceType)) logger.warn(` ⚠ ${op.logicalId} (${op.resourceType}) is a stateful type — the old physical resource's data was destroyed by the replacement and CANNOT be recovered; the re-created resource starts empty.`);
31329
+ if (STATEFUL_TYPES.has(op.resourceType)) logger.warn(` ⚠ ${op.logicalId} (${op.resourceType}) is a stateful type — the old physical resource's data was destroyed by the replacement and is NOT recovered by this rollback; the re-created resource starts empty.`);
30822
31330
  const { provider: createProvider } = ctx.providerRegistry.getProviderFor({
30823
31331
  resourceType: op.resourceType,
30824
31332
  provisionedBy: prev.provisionedBy
@@ -33385,7 +33893,7 @@ var DeployEngine = class {
33385
33893
  const statefulReason = isStatefulRecreateTargetForReplace(resourceType, currentProps);
33386
33894
  if (statefulReason && this.options.forceStatefulRecreation !== true) {
33387
33895
  const immutableProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
33388
- throw new CdkdError(`${logicalId} (${resourceType}) requires replacement (immutable property changed: ${immutableProps}) but it is a stateful resource — ${renderStatefulReason(statefulReason)}. Re-run with --force-stateful-recreation to confirm the data loss, or change the resource definition to avoid the immutable-property change.`, "STATEFUL_REPLACE_BLOCKED");
33896
+ throw markNonRetryable(new CdkdError(`${logicalId} (${resourceType}) requires replacement (immutable property changed: ${immutableProps}) but it is a stateful resource — ${renderStatefulReason(statefulReason)}. Re-run with --force-stateful-recreation to confirm the data loss, or change the resource definition to avoid the immutable-property change.`, "STATEFUL_REPLACE_BLOCKED"));
33389
33897
  }
33390
33898
  }
33391
33899
  let replacementReason;
@@ -33528,9 +34036,10 @@ var DeployEngine = class {
33528
34036
  const ccUnsupported = msg.includes("UnsupportedActionException") || msg.includes("does not support UPDATE");
33529
34037
  const replaceOptIn = updateError instanceof ResourceUpdateNotSupportedError && this.options.replace === true;
33530
34038
  if (ccUnsupported || replaceOptIn) {
33531
- if (replaceOptIn) {
33532
- const statefulReason = isStatefulRecreateTargetForReplace(resourceType, currentProps);
33533
- if (statefulReason && this.options.forceStatefulRecreation !== true) throw new CdkdError(`--replace would DELETE + CREATE the stateful resource ${logicalId} (${resourceType}) ${renderStatefulReason(statefulReason)}. Re-run with --force-stateful-recreation to confirm the data loss, or change the resource definition to avoid the immutable-property change.`, "STATEFUL_REPLACE_BLOCKED");
34039
+ const statefulReason = isStatefulRecreateTargetForReplace(resourceType, currentProps);
34040
+ if (statefulReason && this.options.forceStatefulRecreation !== true) {
34041
+ const retainNote = updateReplacePolicy === "Retain" ? " Note: UpdateReplacePolicy: Retain does NOT protect this paththe replacement deletes the old resource regardless." : "";
34042
+ throw markNonRetryable(new CdkdError((replaceOptIn ? `--replace would DELETE + CREATE the stateful resource ${logicalId} (${resourceType}) — ${renderStatefulReason(statefulReason)}. Re-run with --force-stateful-recreation to confirm the data loss, or change the resource definition to avoid the immutable-property change.` : `${logicalId} (${resourceType}) cannot be updated in place by the provisioning layer it routes through, so applying this change would DELETE + CREATE it — but it is a stateful resource: ${renderStatefulReason(statefulReason)}. Re-run with --force-stateful-recreation to confirm the data loss, or change the resource definition to avoid the update.`) + retainNote, "STATEFUL_REPLACE_BLOCKED", updateError instanceof Error ? updateError : void 0));
33534
34043
  }
33535
34044
  this.logger.info(`UPDATE not supported for ${logicalId} (${resourceType}), replacing (DELETE → CREATE)`);
33536
34045
  const fallbackFinalSnapshotId = await this.prepareFinalSnapshotForDelete(logicalId, resourceType, currentResource, template?.Resources?.[logicalId]?.UpdateReplacePolicy ?? currentResource.updateReplacePolicy);
@@ -34007,7 +34516,10 @@ var DeployEngine = class {
34007
34516
  }
34008
34517
  outputsPassCompleted = true;
34009
34518
  } finally {
34010
- if (context.recordedSecretValues) for (const [value, expr] of context.recordedSecretValues) this.outputSecrets.set(value, expr);
34519
+ if (context.recordedSecretValues) {
34520
+ for (const [value, expr] of context.recordedSecretValues) this.outputSecrets.set(value, expr);
34521
+ mergeResolvedPairs(context.recordedSecretValues, this.outputSecrets);
34522
+ }
34011
34523
  if (!outputsPassCompleted) this.outputsSourceUsable = false;
34012
34524
  }
34013
34525
  for (const [outputKey, output] of Object.entries(template.Outputs)) {
@@ -34028,5 +34540,5 @@ var DeployEngine = class {
34028
34540
  };
34029
34541
 
34030
34542
  //#endregion
34031
- export { DEFAULT_STATE_PREFIX as $, resolveUseCdkBootstrapAssets as $n, __exportAll as $r, isSingleDynamicReferenceToken as $t, bold as A, validateAssetBucketName as An, LocalStartServiceError as Ar, requireConfigObject as At, secretBearingStateKeyWarning as B, runDockerStreaming as Bn, StateError as Br, DiffCalculator as Bt, unsupportedFinalSnapshotError as C, BOOTSTRAP_MARKER_PREFIX as Cn, ConfigError as Cr, assertRegionMatch as Ct, isStatefulRecreateTargetSync as D, isCrossRegionRedirect as Dn, DynamicReferenceRegionAmbiguousError as Dr, readConfigString as Dt, MULTI_REGION_RECREATE_BLOCKED_TYPES as E, getBootstrapMarkerKey as En, DeployCancelledError as Er, configStringRefusal as Et, yellow as F, dockerSpawnEnvWithSensitive as Fn, ProvisioningError as Fr, s3BucketDomainName as Ft, clearOnUpdateRemoval as G, getDefaultStateBucketName as Gn, withErrorHandling as Gr, TemplateParser as Gt, getCurrentResourceSecrets as H, getDockerImageBySourceHash as Hn, formatError as Hr, describeTypeWithThrottleRetry as Ht, collectDeclaredOutputNames as I, formatDockerLoginError as In, ResourceTimeoutError as Ir, s3BucketDualStackDomainName as It, findSilentDropProperties as J, resolveAutoAssetStorage as Jn, isThrottlingError as Jr, TEMPLATE_SOURCED_RULES as Jt, ProviderRegistry as K, getLegacyStateBucketName as Kn, isMarkedNonRetryable as Kr, STATE_SOURCED_CROSS_GENERATION_RULES as Kt, collectPublishedOutputNames as L, getDockerCmd as Ln, ResourceUpdateNotSupportedError as Lr, s3BucketRegionalDomainName as Lt, gray as M, buildDenyExternalAccessPolicy as Mn, MissingCdkCliError as Mr, classifyReplaySecretRegion as Mt, green as N, describeAwsFailure as Nn, NestedStackChildDirectDestroyError as Nr, producerRegionsFromState as Nt, renderStatefulReason as O, parseBootstrapMarker as On, LocalInvokeBuildError as Or, replayWarn as Ot, red as P, buildDockerImage as Pn, PartialFailureError as Pr, s3BucketArn as Pt, CUSTOM_RESOURCE_RESPONSE_PREFIX as Q, resolveStateBucketWithDefaultAndSource as Qn, retryClassificationText as Qr, errorCauseChain as Qt, exportAliasCollisionScrubWarning as R, partitionSensitiveEnv as Rn, StackHasActiveImportsError as Rr, s3BucketWebsiteUrl as Rt, refusesFinalSnapshot as S, AssetModeResolver as Sn, CdkdError as Sr, resolveExplicitPhysicalId as St, extractDeploymentEventError as T, ensureAssetStorage as Tn, DependencyError as Tr, configBooleanRefusal as Tt, IAMRoleProvider as U, Synthesizer as Un, isCdkdError as Ur, withRetry as Ut, stateKeySecretExposure as V, AssetManifestLoader as Vn, SynthesisError as Vr, INTRINSIC_KEYS as Vt, collectInlinePolicyNamesManagedBySiblings as W, synthesisStatusMessage as Wn, normalizeAwsError as Wr, DagBuilder as Wt, maskDeep as X, resolveSkipPrefix as Xn, markNonRetryable as Xr, createSecretMasker as Xt, createMaskedRetryLogger as Y, resolveCaptureObservedState as Yn, isTransientServerError as Yr, carriesSecretMask as Yt, maskerOrIdentity as Z, resolveStateBucketWithDefault as Zn, markRedactedCause as Zr, dynamicReferenceTokens as Zt, PRE_DELETE_SNAPSHOT_TYPES as _, createAssetRedirectResolver as _n, AwsClients as _r, isUnboundTemplateParameter as _t, DeploymentEventsStore as a, scrubResourceRecord as an, findLargeInlineResources as ar, CloudControlProvider as at, createPreDeleteFinalSnapshot as b, escapeRegExp$1 as bn, setAwsClients as br, WAFv2WebACLProvider as bt, replayFailedOperations as c, rebuildClientForBucketRegion as cn, displaySafe as cr, deleteIndeterminateGuards as ct, updatePartialReason as d, importableOutputs as dn, canonicalizeRegion as dr, isTerminationProtectionPropagationError as dt, maskSecretsInError as en, stateBucketExistenceConfirmed as er, beginCommandInterruptScope as et, withResourceDeadline as f, shouldRetainResource as fn, derivePartitionAndUrlSuffix as fr, IntrinsicFunctionResolver as ft, ATOMIC_FINAL_SNAPSHOT_TYPES as g, buildAssetRedirectMap as gn, resolveBucketRegion as gr, getAccountInfo as gt, computeImplicitDeleteEdges as h, WorkGraph as hn, clearBucketRegionCache as hr, coerceParameterTypedValue as ht, DeploymentEventsReader as i, redactSecretsForState as in, MIGRATE_TMP_PREFIX as ir, startInterruptWatch as it, cyan as j, validateContainerRepoName as jn, LockError as jr, requireConfigString as jt, formatResourceLine as k, readBootstrapMarkerBody as kn, LocalMigrateError as kr, requireConfigArray as kt, replayRollback as l, exportNamesCarriedFrom as ln, expectedOwnerParam as lr, deleteSkipReason as lt, IMPLICIT_DELETE_DEPENDENCIES as m, stringifyValue as mn, processStackMessages as mr, cfnRefValueFromPhysicalId as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, recordMaskOnlyValue as nn, CFN_TEMPLATE_BODY_LIMIT as nr, interruptWatchListenerCount as nt, planFailedOps as o, LockManager as on, uploadCfnTemplate as or, slowCcOperationTimeoutMs as ot, maskingRetryLogger as p, AssetPublisher as pn, AssemblyReader as pr, carriesDynamicReference as pt, findActionableSilentDrops as q, resolveApp as qn, isRetryableTransientError as qr, STATE_SOURCED_READBACK_RULES as qt, DeployEngine as r, recoverMaskedOutput as rn, CFN_TEMPLATE_URL_LIMIT as rr, isInterruptedWaitError as rt, planRollback as s, S3StateBackend as sn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as sr, UNSPECIFIED_SKIP_REASON as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, maskSecretsInText as tn, warnDeprecatedNoPrefixCliFlag as tr, endCommandInterruptScope as tt, updatePartialMessage as u, importableOutputKeys as un, PARTITION_TABLE as ur, disableInstanceApiTermination as ut, buildFinalSnapshotIdentifier as v, loadPublishableAssetManifest as vn, getAwsClients as vr, parameterTypeMayLoseSecretIdentity as vt, makeCanonicalizePropertiesFn as w, assertAssetBucketRegion as wn, CrossAccountSecretRefusalError as wr, coerceCfnBoolean as wt, isFinalSnapshotError as x, stripControlChars as xn, AssetError as xr, normalizeAwsTagsToCfn as xt, ccRoutedFinalSnapshotError as y, rewriteTemplateAssetReferences as yn, resetAwsClients as yr, refStateLookupFromResource as yt, isExportAliasCollision as z, runDockerForeground as zn, StackTerminationProtectionError as zr, applyRoleArnIfSet as zt };
34032
- //# sourceMappingURL=deploy-engine-DhMm2M33.js.map
34543
+ export { endCommandInterruptScope as $, resolveCaptureObservedState as $n, isThrottlingError as $r, maskSecretsInText as $t, bold as A, getBootstrapMarkerKey as An, DeployCancelledError as Ar, classifyReplaySecretRegion as At, secretBearingStateKeyWarning as B, formatDockerLoginError as Bn, ProvisioningError as Br, describeTypeWithThrottleRetry as Bt, unsupportedFinalSnapshotError as C, rewriteTemplateAssetReferences as Cn, resetAwsClients as Cr, configBooleanRefusal as Ct, isStatefulRecreateTargetSync as D, BOOTSTRAP_MARKER_PREFIX as Dn, ConfigError as Dr, requireConfigArray as Dt, MULTI_REGION_RECREATE_BLOCKED_TYPES as E, AssetModeResolver as En, CdkdError as Er, replayWarn as Et, yellow as F, validateContainerRepoName as Fn, LocalStartServiceError as Fr, s3BucketRegionalDomainName as Ft, clearOnUpdateRemoval as G, AssetManifestLoader as Gn, StateError as Gr, STATE_SOURCED_READBACK_RULES as Gt, getCurrentResourceSecrets as H, partitionSensitiveEnv as Hn, ResourceUpdateNotSupportedError as Hr, DagBuilder as Ht, collectDeclaredOutputNames as I, buildDenyExternalAccessPolicy as In, LockError as Ir, s3BucketWebsiteUrl as It, findSilentDropProperties as J, synthesisStatusMessage as Jn, isCdkdError as Jr, createSecretMasker as Jt, ProviderRegistry as K, getDockerImageBySourceHash as Kn, SynthesisError as Kr, TEMPLATE_SOURCED_RULES as Kt, collectPublishedOutputNames as L, describeAwsFailure as Ln, MissingCdkCliError as Lr, applyRoleArnIfSet as Lt, gray as M, parseBootstrapMarker as Mn, IntrinsicResolutionRefusalError as Mr, s3BucketArn as Mt, green as N, readBootstrapMarkerBody as Nn, LocalInvokeBuildError as Nr, s3BucketDomainName as Nt, renderStatefulReason as O, assertAssetBucketRegion as On, CrossAccountSecretRefusalError as Or, requireConfigObject as Ot, red as P, validateAssetBucketName as Pn, LocalMigrateError as Pr, s3BucketDualStackDomainName as Pt, beginCommandInterruptScope as Q, resolveAutoAssetStorage as Qn, isRetryableTransientError as Qr, maskSecretsInError as Qt, exportAliasCollisionScrubWarning as R, buildDockerImage as Rn, NestedStackChildDirectDestroyError as Rr, DiffCalculator as Rt, refusesFinalSnapshot as S, loadPublishableAssetManifest as Sn, getAwsClients as Sr, coerceCfnBoolean as St, extractDeploymentEventError as T, stripControlChars as Tn, AssetError as Tr, readConfigString as Tt, IAMRoleProvider as U, runDockerForeground as Un, StackHasActiveImportsError as Ur, TemplateParser as Ut, stateKeySecretExposure as V, getDockerCmd as Vn, ResourceTimeoutError as Vr, withRetry as Vt, collectInlinePolicyNamesManagedBySiblings as W, runDockerStreaming as Wn, StackTerminationProtectionError as Wr, STATE_SOURCED_CROSS_GENERATION_RULES as Wt, maskDeep as X, getLegacyStateBucketName as Xn, withErrorHandling as Xr, errorCauseChain as Xt, createMaskedRetryLogger as Y, getDefaultStateBucketName as Yn, normalizeAwsError as Yr, dynamicReferenceTokens as Yt, maskerOrIdentity as Z, resolveApp as Zn, isMarkedNonRetryable as Zr, isSingleDynamicReferenceToken as Zt, PRE_DELETE_SNAPSHOT_TYPES as _, AssetPublisher as _n, AssemblyReader as _r, refStateLookupFromResource as _t, DeploymentEventsStore as a, S3StateBackend as an, warnDeprecatedNoPrefixCliFlag as ar, UNSPECIFIED_SKIP_REASON as at, createPreDeleteFinalSnapshot as b, buildAssetRedirectMap as bn, resolveBucketRegion as br, resolveExplicitPhysicalId as bt, replayFailedOperations as c, buildForceUnlockCommand as cn, MIGRATE_TMP_PREFIX as cr, disableInstanceApiTermination as ct, updatePartialReason as d, CUSTOM_RESOURCE_RESPONSE_PREFIX as dn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as dr, carriesDynamicReference as dt, isTransientServerError as ei, recordMaskOnlyValue as en, resolveSkipPrefix as er, interruptWatchListenerCount as et, withResourceDeadline as f, DEFAULT_STATE_PREFIX as fn, displaySafe as fr, cfnRefValueFromPhysicalId as ft, ATOMIC_FINAL_SNAPSHOT_TYPES as g, shouldRetainResource as gn, derivePartitionAndUrlSuffix as gr, parameterTypeMayLoseSecretIdentity as gt, computeImplicitDeleteEdges as h, importableOutputs as hn, canonicalizeRegion as hr, isUnboundTemplateParameter as ht, DeploymentEventsReader as i, __exportAll as ii, LockManager as in, stateBucketExistenceConfirmed as ir, slowCcOperationTimeoutMs as it, cyan as j, isCrossRegionRedirect as jn, DynamicReferenceRegionAmbiguousError as jr, producerRegionsFromState as jt, formatResourceLine as k, ensureAssetStorage as kn, DependencyError as kr, requireConfigString as kt, replayRollback as l, buildLockContentionMessage as ln, findLargeInlineResources as lr, isTerminationProtectionPropagationError as lt, IMPLICIT_DELETE_DEPENDENCIES as m, importableOutputKeys as mn, PARTITION_TABLE as mr, getAccountInfo as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, markRedactedCause as ni, redactSecretsForState as nn, resolveStateBucketWithDefaultAndSource as nr, startInterruptWatch as nt, planFailedOps as o, rebuildClientForBucketRegion as on, CFN_TEMPLATE_BODY_LIMIT as or, deleteIndeterminateGuards as ot, maskingRetryLogger as p, exportNamesCarriedFrom as pn, expectedOwnerParam as pr, coerceParameterTypedValue as pt, findActionableSilentDrops as q, Synthesizer as qn, formatError as qr, carriesSecretMask as qt, DeployEngine as r, retryClassificationText as ri, scrubResourceRecord as rn, resolveUseCdkBootstrapAssets as rr, CloudControlProvider as rt, planRollback as s, UNRENDERABLE as sn, CFN_TEMPLATE_URL_LIMIT as sr, deleteSkipReason as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, markNonRetryable as ti, recoverMaskedOutput as tn, resolveStateBucketWithDefault as tr, isInterruptedWaitError as tt, updatePartialMessage as u, forceQuitRecoveryClause as un, uploadCfnTemplate as ur, IntrinsicFunctionResolver as ut, buildFinalSnapshotIdentifier as v, stringifyValue as vn, processStackMessages as vr, WAFv2WebACLProvider as vt, makeCanonicalizePropertiesFn as w, escapeRegExp$1 as wn, setAwsClients as wr, configStringRefusal as wt, isFinalSnapshotError as x, createAssetRedirectResolver as xn, AwsClients as xr, assertRegionMatch as xt, ccRoutedFinalSnapshotError as y, WorkGraph as yn, clearBucketRegionCache as yr, normalizeAwsTagsToCfn as yt, isExportAliasCollision as z, dockerSpawnEnvWithSensitive as zn, PartialFailureError as zr, INTRINSIC_KEYS as zt };
34544
+ //# sourceMappingURL=deploy-engine-DPh-XamZ.js.map