@go-to-k/cdkd 0.290.2 → 0.290.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,6 +1,6 @@
1
- import { b as displaySafe, d as applyDefaultNameForFallback, f as explicitNamePropertyFor, g as looksLikeCdkdGeneratedName, h as getCurrentStackName, m as generateResourceNameWithFallback, n as getLogger, p as generateResourceName, r as isStdoutReservedForPayload, s as getLiveRenderer, v as withStackName, y as UNRENDERABLE } from "./logger-DTEUzYP_.js";
1
+ import { b as displayIdent, d as applyDefaultNameForFallback, f as explicitNamePropertyFor, g as looksLikeCdkdGeneratedName, h as getCurrentStackName, m as generateResourceNameWithFallback, n as getLogger, p as generateResourceName, r as isStdoutReservedForPayload, s as getLiveRenderer, v as withStackName, x as displaySafe, y as UNRENDERABLE } from "./logger-th1myfgh.js";
2
2
  import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
3
- import { t as getCdkdVersion } from "./version-7XwrjRzX.js";
3
+ import { t as getCdkdVersion } from "./version-BTao4oxo.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetBucketReplicationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -17,7 +17,7 @@ import { APIGatewayClient, GetRestApiCommand } from "@aws-sdk/client-api-gateway
17
17
  import { DescribeApiDestinationCommand, DescribeConnectionCommand, EventBridgeClient } from "@aws-sdk/client-eventbridge";
18
18
  import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
19
19
  import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
20
- import { CloudFrontClient, GetCloudFrontOriginAccessIdentityCommand } from "@aws-sdk/client-cloudfront";
20
+ import { CloudFrontClient, GetCloudFrontOriginAccessIdentityCommand, GetDistributionCommand } from "@aws-sdk/client-cloudfront";
21
21
  import { CloudWatchClient } from "@aws-sdk/client-cloudwatch";
22
22
  import { CloudWatchLogsClient } from "@aws-sdk/client-cloudwatch-logs";
23
23
  import { BedrockAgentCoreControlClient } from "@aws-sdk/client-bedrock-agentcore-control";
@@ -1148,7 +1148,15 @@ function formatDuration$1(ms) {
1148
1148
  * `Outputs.<Key>` the child does not declare rather than letting
1149
1149
  * `guardedPhysicalIdFallback` serve the synthetic
1150
1150
  * `arn:cdkd-local:...` physical id — that id starts with `arn:`, so the
1151
- * #1103 ARN-shape guard passes it.
1151
+ * #1103 ARN-shape guard passes it; and the LIVE-READ refusals of issue
1152
+ * [#3096](https://github.com/go-to-k/cdkd/issues/3096)
1153
+ * (`refuseUnservedAttribute`, raised by the `AWS::EC2::Instance` address /
1154
+ * DNS / zone arm, the `AWS::EC2::VPC` `DefaultSecurityGroup` arm and the
1155
+ * `AWS::CloudFront::Distribution` `DomainName` arm when the record omits
1156
+ * the attribute and the live read finds nothing or fails), plus the
1157
+ * read-less `AWS::RDS::DBProxy` / `DBProxyEndpoint` `VpcId` refusal beside
1158
+ * them — each replacing a physical id the attribute's value type can never
1159
+ * accept.
1152
1160
  * 2. NOT reachable from it, and thrown as this class only so that "deliberate
1153
1161
  * refusal" is a property of the THROW rather than of the one catch that
1154
1162
  * inspects it: `resolveSplit`'s two refusals of a non-string value (#1874),
@@ -1177,11 +1185,17 @@ function formatDuration$1(ms) {
1177
1185
  * subclass, never on this class.
1178
1186
  *
1179
1187
  * The class is deliberately NOT `markNonRetryable` at construction, unlike
1180
- * {@link ResourceUpdateNotSupportedError}: EXACTLY ONE of its throw sites is
1188
+ * {@link ResourceUpdateNotSupportedError}: two kinds of throw site are
1181
1189
  * genuinely time-dependent — the fabricated-account guard, where
1182
1190
  * `getAccountInfo` caches a fabricated answer for only 10s precisely so a
1183
- * later attempt can heal — so a constructor-level marker would wrongly make
1184
- * that one terminal. Every OTHER site marks at its own `throw`: each
1191
+ * later attempt can heal, and since issue #3096 the LIVE-READ refusals
1192
+ * (`refuseUnservedAttribute`), where a `pending` instance settles seconds
1193
+ * later and a failed describe can succeed on the next attempt — so a
1194
+ * constructor-level marker would wrongly DECLARE those terminal (whether
1195
+ * `withRetry` then retries one is the classifiers' call, and that helper's
1196
+ * doc comment says what they actually see). Every OTHER
1197
+ * site marks at its own `throw` (the #3096 `DBProxy` `VpcId` refusal
1198
+ * included — it has no live read, so it decides from the record alone): each
1185
1199
  * decides from inputs a retry cannot change (a persisted state record, an
1186
1200
  * attribute-name suffix, a CLI flag, an already-resolved value's type, a
1187
1201
  * template's literal `RoleArn`, a child stack's declared output names), and
@@ -12731,7 +12745,13 @@ const STATE_SOURCED_READBACK_RULES = {
12731
12745
  * `observedProperties`, where a mask is a REGRESSION rather than a refusal
12732
12746
  * (`cdkd export` blocks such a record and the rollback replay refuses the
12733
12747
  * operation). A caller declares this constant when it knows its bag becomes a
12734
- * drift baseline and nothing else.
12748
+ * drift baseline and nothing else — and since issue
12749
+ * [#2939](https://github.com/go-to-k/cdkd/issues/2939) the two `cdkd drift`
12750
+ * writers make that call per record: `--accept`'s new baseline and
12751
+ * `--revert`'s narrowed delta take THIS constant when the record carries
12752
+ * `observedProperties` (the bag lands there) and
12753
+ * {@link STATE_SOURCED_READBACK_RULES} otherwise (the bag lands in
12754
+ * `properties`), the *** rows of the write-site table above.
12735
12755
  */
12736
12756
  const STATE_SOURCED_BASELINE_RULES = {
12737
12757
  descendArrays: false,
@@ -16981,6 +17001,7 @@ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16981
17001
  * releases. Neither counter feeds a control decision; they are reporting only.
16982
17002
  */
16983
17003
  async function withRetry(operation, logicalId, opts = {}) {
17004
+ const shownId = displaySafe(logicalId, { asciiOnly: true }) || "<unrenderable>";
16984
17005
  const maxRetries = opts.maxRetries ?? 8;
16985
17006
  const initialDelayMs = opts.initialDelayMs ?? 1e3;
16986
17007
  const maxDelayMs = opts.maxDelayMs ?? 8e3;
@@ -17014,7 +17035,7 @@ async function withRetry(operation, logicalId, opts = {}) {
17014
17035
  if (propagationRetries > 0) spent.push(`${propagationRetries} IAM-propagation ${propagationRetries === 1 ? "retry" : "retries"} over ${(propagationSleptMs / 1e3).toFixed(2)}s of propagation backoff${budgetExhausted ? " (the full propagation budget)" : ""}`);
17015
17036
  if (nameCooldownRetries > 0) spent.push(`${nameCooldownRetries} name-cooldown ${nameCooldownRetries === 1 ? "retry" : "retries"} over ${(nameCooldownSleptMs / 1e3).toFixed(2)}s waiting for the name to be released${nameCooldownBudgetExhausted ? " (the full name-cooldown budget)" : ""}`);
17016
17037
  if (serverErrorRetries > 0) spent.push(`${serverErrorRetries} transient server-error ${serverErrorRetries === 1 ? "retry" : "retries"} (HTTP 5xx)`);
17017
- const summary = () => `${logicalId}: gave up after ${spent.join(" and ")} - ${message}` + formatRetryClassificationSignals(error);
17038
+ const summary = () => `${shownId}: gave up after ${spent.join(" and ")} - ${message}` + formatRetryClassificationSignals(error);
17018
17039
  try {
17019
17040
  opts.logger?.warn?.(summary());
17020
17041
  } catch {}
@@ -17023,7 +17044,7 @@ async function withRetry(operation, logicalId, opts = {}) {
17023
17044
  }
17024
17045
  const delay = propagation ? Math.min(250 * Math.pow(2, attempt), IAM_PROPAGATION_MAX_DELAY_MS) : nameCooldown ? Math.min(NAME_COOLDOWN_INITIAL_DELAY_MS * Math.pow(2, attempt), NAME_COOLDOWN_MAX_DELAY_MS) : Math.min(initialDelayMs * Math.pow(2, attempt), maxDelayMs);
17025
17046
  const backoffThroughThisAttemptMs = propagation ? propagationSleptMs + delay : propagationSleptMs;
17026
- opts.logger?.debug(` ⏳ Retrying ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${attemptLimit}${propagation ? `, ${(backoffThroughThisAttemptMs / 1e3).toFixed(2)}s backoff through this attempt` : ""}) - ${message}`);
17047
+ opts.logger?.debug(` ⏳ Retrying ${shownId} in ${delay / 1e3}s (attempt ${attempt + 1}/${attemptLimit}${propagation ? `, ${(backoffThroughThisAttemptMs / 1e3).toFixed(2)}s backoff through this attempt` : ""}) - ${message}`);
17027
17048
  for (let waited = 0; waited < delay; waited += 1e3) {
17028
17049
  if (opts.isInterrupted?.()) throw opts.onInterrupted ? opts.onInterrupted() : /* @__PURE__ */ new Error("Interrupted");
17029
17050
  await sleep(Math.min(1e3, delay - waited));
@@ -20675,14 +20696,17 @@ async function assumeRoleForCrossAccountStateRead(roleArn) {
20675
20696
  * up the assumed-role credentials automatically without touching the
20676
20697
  * client construction sites.
20677
20698
  *
20678
- * **Why cdkd needs admin-equivalent on the assumed role.** Unlike `cdk
20679
- * deploy`, cdkd does NOT route through CloudFormation. There is no
20680
- * cfn-exec-role to delegate to. Every IAM / EC2 / Lambda / etc. API
20681
- * call is issued from the cdkd process directly. The role you pass to
20682
- * `--role-arn` (or set in `CDKD_ROLE_ARN`) MUST therefore have
20683
- * admin-equivalent permissions on the resources being deployed; CDK
20684
- * CLI's `cdk-hnb659fds-deploy-role-*` is NOT sufficient — that role
20685
- * only carries CFn + asset-publish permissions.
20699
+ * **What the assumed role must carry.** Unlike `cdk deploy`, cdkd does
20700
+ * NOT route through CloudFormation. There is no cfn-exec-role to
20701
+ * delegate to. Every IAM / EC2 / Lambda / etc. API call is issued from
20702
+ * the cdkd process directly. The role you pass to `--role-arn` (or set
20703
+ * in `CDKD_ROLE_ARN`) must therefore carry the actions for the resource
20704
+ * types being deployed plus cdkd's own bookkeeping set (Cloud Control,
20705
+ * `cloudformation:DescribeType` / `ListExports` / `DescribeStacks`,
20706
+ * `sts:GetCallerIdentity`, the state and asset storage) — i.e. exactly what the
20707
+ * caller's own principal would otherwise have needed, not
20708
+ * `AdministratorAccess`. CDK CLI's `cdk-hnb659fds-deploy-role-*` is NOT
20709
+ * sufficient — that role only carries CFn + asset-publish permissions.
20686
20710
  *
20687
20711
  * Default session duration is 1 hour. For longer-running deploys, the
20688
20712
  * caller should re-issue the cdkd command (the in-flight credentials
@@ -22663,6 +22687,36 @@ var WAFv2WebACLProvider = class {
22663
22687
  }
22664
22688
  };
22665
22689
 
22690
+ //#endregion
22691
+ //#region src/provisioning/ec2-instance-state.ts
22692
+ /**
22693
+ * The ONE spelling of "has this EC2 instance SETTLED?" (issue
22694
+ * [#3096](https://github.com/go-to-k/cdkd/issues/3096) review).
22695
+ *
22696
+ * Two readers must agree on it or a record and a resolution disagree about
22697
+ * the same instance: `EC2Provider`'s `describedInstanceAttributes` decides
22698
+ * whether a missing public member is RECORDED as the known `''` (settled) or
22699
+ * OMITTED (still `pending`, issue #3077), and the resolver's
22700
+ * `AWS::EC2::Instance` live arm decides, for an omitted member, whether to
22701
+ * SERVE that same `''` or REFUSE. Each carried the comparison inline; this
22702
+ * module is the shared predicate so a change to one side cannot leave the
22703
+ * other on the old rule.
22704
+ *
22705
+ * `pending` is the only state in which EC2 has not finished assigning the
22706
+ * launch-time members; every other reported state — `running`, `stopping`,
22707
+ * `stopped`, `shutting-down`, `terminated` — is a settled one whose public
22708
+ * pair, when absent, is genuinely absent (a private-subnet instance, or a
22709
+ * stopped one whose address was released). NO state at all is treated like
22710
+ * `pending`: a read-back that returned no instance, or a shape without one,
22711
+ * says nothing about having settled.
22712
+ *
22713
+ * A LEAF: no imports, so both `src/provisioning/**` and `src/deployment/**`
22714
+ * can take it without adding an edge between the two layers.
22715
+ */
22716
+ function isSettledInstanceState(stateName) {
22717
+ return stateName !== void 0 && stateName !== "pending";
22718
+ }
22719
+
22666
22720
  //#endregion
22667
22721
  //#region src/deployment/intrinsic-function-resolver.ts
22668
22722
  /**
@@ -23195,6 +23249,19 @@ function withoutProducerRegions(context) {
23195
23249
  return rest;
23196
23250
  }
23197
23251
  /**
23252
+ * Masked log twins registered this pass, per pass bag (issue #3100;
23253
+ * `rememberLogTwin` / `logTwinOfProduct`). Log-only: nothing that resolves or
23254
+ * persists a value reads it.
23255
+ *
23256
+ * MODULE scope, not instance scope: a region-pinned sibling resolver
23257
+ * (`resolverForProducerRegion`) resolves on behalf of the consumer with the
23258
+ * consumer's bag, so an instance-local registry left the sibling's twin where
23259
+ * the consumer's `Fn::Join` / `Fn::Sub` never looked. The key is still the
23260
+ * pass's own bag object, so scope does not widen: another pass holds another
23261
+ * bag and cannot reach these entries, and they die with the bag.
23262
+ */
23263
+ const LOG_TWINS_BY_PASS = /* @__PURE__ */ new WeakMap();
23264
+ /**
23198
23265
  * Does `value` carry a CloudFormation dynamic reference anywhere inside it?
23199
23266
  *
23200
23267
  * Used as the identity fast path of {@link
@@ -23313,10 +23380,37 @@ const recordedSecretExpressions = {
23313
23380
  * lookup (PrivateIp / PublicIp / PrivateDnsName / PublicDnsName /
23314
23381
  * AvailabilityZone). Keyed by `${physicalId}#${attributeName}`. The IP /
23315
23382
  * DNS attributes are not derivable from the instance id, so they are read
23316
- * back from AWS once per (instance, attribute) and memoized for the deploy
23317
- * lifetime.
23318
- */
23319
- const cachedEc2InstanceAttributes = {};
23383
+ * back from AWS once per (instance, attribute) and memoized for the PROCESS
23384
+ * lifetime — one `cdkd deploy` per CLI process, so "the deploy" in practice;
23385
+ * nothing in `src/` calls `resetAccountInfoCache`, tests do. Only a VALUE is cached — a settled instance's address, or the
23386
+ * known-empty `''` a settled instance reports for a public member it has
23387
+ * none of — never a refusal: a `pending` instance is re-described on the
23388
+ * next resolution, when it may have settled (issue #3096).
23389
+ */
23390
+ const cachedEc2InstanceAttributes = Object.create(null);
23391
+ /**
23392
+ * The two sibling caches of {@link cachedEc2InstanceAttributes} (issue
23393
+ * #3096), each holding a value the resolver read LIVE because the state
23394
+ * record omitted it — `definedAttributes` drops a member the provider could
23395
+ * not read back (issue #3077), so the `Fn::GetAtt` falls out of the flat
23396
+ * lookup and into `constructAttribute`'s per-type arm. Keyed by physical id
23397
+ * (AWS-minted, unique across accounts and regions, and the value — a default
23398
+ * security group id, a distribution hostname — never changes); like the
23399
+ * instance cache, a refusal caches nothing. Same process lifetime, cleared
23400
+ * together with it by {@link resetAccountInfoCache}. The RDS `DBProxy` / `DBProxyEndpoint`
23401
+ * `VpcId` arms have no cache because they have no live read: `AwsClients`
23402
+ * exposes no RDS client and this file imports none, so those two arms refuse
23403
+ * outright (see `refuseUnservedAttribute`'s callers).
23404
+ *
23405
+ * All three are NULL-PROTOTYPE objects (#3096 delta review, measured): the
23406
+ * key is a state-record physical id, and on a plain `{}` a record holding
23407
+ * `constructor` / `__proto__` / `hasOwnProperty` read a FUNCTION out of
23408
+ * `Object.prototype` as the cached value — served with zero AWS calls, ahead
23409
+ * of every shape guard below. `Object.create(null)` has nothing to read; the
23410
+ * `delete`-based clears in `resetAccountInfoCache` keep the prototype.
23411
+ */
23412
+ const cachedVpcDefaultSecurityGroups = Object.create(null);
23413
+ const cachedCloudFrontDomainNames = Object.create(null);
23320
23414
  /**
23321
23415
  * The region this call answers for: the caller's override, else the ambient one.
23322
23416
  *
@@ -25228,7 +25322,44 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
25228
25322
  this.logger.warn(`Failed to fetch VPC Ipv6CidrBlocks for ${physicalId}: ${error instanceof Error ? error.message : String(error)}`);
25229
25323
  return [];
25230
25324
  }
25231
- case "DefaultSecurityGroup": return resource.attributes?.["DefaultSecurityGroup"] || physicalId;
25325
+ case "DefaultSecurityGroup": {
25326
+ if (!/^vpc-[0-9a-f]+$/.test(physicalId)) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, DefaultSecurityGroup] for AWS::EC2::VPC: the state record's physical id "${displaySafe(this.maskThenStripThenMask(physicalId, context)).slice(0, 64)}" is not a VPC id (vpc-<hex>), so cdkd will not use it as an EC2 filter value. Repair the record (cdkd import, or re-create the VPC) and deploy again.`));
25327
+ const cachedGroupId = cachedVpcDefaultSecurityGroups[physicalId];
25328
+ if (cachedGroupId !== void 0) return cachedGroupId;
25329
+ let groupId;
25330
+ try {
25331
+ groupId = (await this.clientsForRegion(this.explicitRegion).ec2.send(new DescribeSecurityGroupsCommand({ Filters: [{
25332
+ Name: "vpc-id",
25333
+ Values: [physicalId]
25334
+ }, {
25335
+ Name: "group-name",
25336
+ Values: ["default"]
25337
+ }] }))).SecurityGroups?.[0]?.GroupId;
25338
+ } catch (err) {
25339
+ this.refuseUnservedAttribute({
25340
+ logicalId,
25341
+ attributeName,
25342
+ resourceType,
25343
+ physicalId,
25344
+ context,
25345
+ observed: this.describeFailureObserved("DescribeSecurityGroups", err, context),
25346
+ remedy: "Fix the read (the vpc-id / group-name filtered DescribeSecurityGroups permission, or the region) and deploy again."
25347
+ });
25348
+ }
25349
+ if (groupId) {
25350
+ cachedVpcDefaultSecurityGroups[physicalId] = groupId;
25351
+ return groupId;
25352
+ }
25353
+ this.refuseUnservedAttribute({
25354
+ logicalId,
25355
+ attributeName,
25356
+ resourceType,
25357
+ physicalId,
25358
+ context,
25359
+ observed: "DescribeSecurityGroups found no group named \"default\" in the VPC",
25360
+ remedy: "Check the VPC in the console, or reference the attribute from a later deploy."
25361
+ });
25362
+ }
25232
25363
  default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
25233
25364
  }
25234
25365
  if (resourceType === "AWS::IAM::Policy") switch (attributeName) {
@@ -25423,9 +25554,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
25423
25554
  const cacheKey = `${physicalId}#${attributeName}`;
25424
25555
  const cached = cachedEc2InstanceAttributes[cacheKey];
25425
25556
  if (cached !== void 0) return cached;
25557
+ let value;
25558
+ let stateName;
25426
25559
  try {
25427
25560
  const instance = (await this.clientsForRegion(this.explicitRegion).ec2.send(new DescribeInstancesCommand({ InstanceIds: [physicalId] }))).Reservations?.[0]?.Instances?.[0];
25428
- let value;
25561
+ stateName = instance?.State?.Name;
25429
25562
  switch (attributeName) {
25430
25563
  case "PrivateIp":
25431
25564
  value = instance?.PrivateIpAddress;
@@ -25441,15 +25574,36 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
25441
25574
  break;
25442
25575
  case "AvailabilityZone": value = instance?.Placement?.AvailabilityZone;
25443
25576
  }
25444
- if (value !== void 0 && value !== null && value !== "") {
25445
- cachedEc2InstanceAttributes[cacheKey] = value;
25446
- return value;
25447
- }
25448
- this.logger.warn(`DescribeInstances(${physicalId}) returned no ${this.maskSecretsForLog(attributeName, context)}; returning physical ID`);
25449
25577
  } catch (err) {
25450
- this.logger.warn(`DescribeInstances(${physicalId}) failed for ${this.maskSecretsForLog(attributeName, context)}: ${this.maskSecretsForLog(err instanceof Error ? err.message : String(err), context)}`);
25578
+ this.refuseUnservedAttribute({
25579
+ logicalId,
25580
+ attributeName,
25581
+ resourceType,
25582
+ physicalId,
25583
+ context,
25584
+ observed: this.describeFailureObserved("DescribeInstances", err, context),
25585
+ remedy: "Fix the read (the ec2:DescribeInstances permission, or the region) and deploy again."
25586
+ });
25451
25587
  }
25452
- return physicalId;
25588
+ if (value !== void 0 && value !== null && value !== "") {
25589
+ cachedEc2InstanceAttributes[cacheKey] = value;
25590
+ return value;
25591
+ }
25592
+ const settled = isSettledInstanceState(stateName);
25593
+ if (settled && (attributeName === "PublicIp" || attributeName === "PublicDnsName")) {
25594
+ cachedEc2InstanceAttributes[cacheKey] = "";
25595
+ return "";
25596
+ }
25597
+ const observedState = stateName === void 0 ? "no instance state" : `state ${stateName}`;
25598
+ this.refuseUnservedAttribute({
25599
+ logicalId,
25600
+ attributeName,
25601
+ resourceType,
25602
+ physicalId,
25603
+ context,
25604
+ observed: `DescribeInstances reports no ${this.maskSecretsForLog(attributeName, context)} yet (${observedState})`,
25605
+ remedy: settled ? "Check the instance in the console; a terminated or stopped instance has no such attribute to serve." : "Deploy without --no-wait so the instance is running before its attributes are read, or reference the attribute from a later deploy once it is."
25606
+ });
25453
25607
  }
25454
25608
  default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
25455
25609
  }
@@ -25467,9 +25621,115 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
25467
25621
  if (attributeName === "LaunchTemplateId") return physicalId;
25468
25622
  return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
25469
25623
  }
25624
+ if (resourceType === "AWS::CloudFront::Distribution") switch (attributeName) {
25625
+ case "Id": return physicalId;
25626
+ case "DomainName": {
25627
+ if (!/^[A-Z0-9]+$/.test(physicalId)) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, DomainName] for AWS::CloudFront::Distribution: the state record's physical id "${displaySafe(this.maskThenStripThenMask(physicalId, context)).slice(0, 64)}" is not a distribution id (upper-case alphanumerics), so cdkd will not look it up. Repair the record (cdkd import, or re-create the distribution) and deploy again.`));
25628
+ const cachedDomainName = cachedCloudFrontDomainNames[physicalId];
25629
+ if (cachedDomainName !== void 0) return cachedDomainName;
25630
+ let domainName;
25631
+ try {
25632
+ domainName = (await this.clientsForRegion(this.explicitRegion).cloudFront.send(new GetDistributionCommand({ Id: physicalId }))).Distribution?.DomainName;
25633
+ } catch (err) {
25634
+ this.refuseUnservedAttribute({
25635
+ logicalId,
25636
+ attributeName,
25637
+ resourceType,
25638
+ physicalId,
25639
+ context,
25640
+ observed: this.describeFailureObserved("GetDistribution", err, context),
25641
+ remedy: "Fix the read (the cloudfront:GetDistribution permission) and deploy again."
25642
+ });
25643
+ }
25644
+ if (domainName) {
25645
+ cachedCloudFrontDomainNames[physicalId] = domainName;
25646
+ return domainName;
25647
+ }
25648
+ this.refuseUnservedAttribute({
25649
+ logicalId,
25650
+ attributeName,
25651
+ resourceType,
25652
+ physicalId,
25653
+ context,
25654
+ observed: "GetDistribution reports no DomainName",
25655
+ remedy: "Check the distribution in the console, or reference the attribute from a later deploy."
25656
+ });
25657
+ }
25658
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
25659
+ }
25660
+ if (resourceType === "AWS::RDS::DBProxy" || resourceType === "AWS::RDS::DBProxyEndpoint") {
25661
+ if (attributeName === "VpcId") throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resourceType}: the state record holds no VpcId for it (the read-back that would have recorded it reported none), and the physical id "${displaySafe(this.maskThenStripThenMask(physicalId, context))}" is a name, not a VPC id, so cdkd refuses to substitute it. Update the resource so its next deploy records the value, or reference the VPC directly.`));
25662
+ return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
25663
+ }
25470
25664
  return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
25471
25665
  }
25472
25666
  /**
25667
+ * Refuse an attribute the state record omits and a live read could not
25668
+ * serve (issue [#3096](https://github.com/go-to-k/cdkd/issues/3096)).
25669
+ *
25670
+ * The arms that call this all share one shape: the provider recorded no
25671
+ * value because AWS had not assigned one yet or the read-back failed (issue
25672
+ * #3077 made the key ABSENT rather than `''`), the resolver re-reads it
25673
+ * live, and the read finds nothing — or fails. Before #3096 every such arm
25674
+ * answered the PHYSICAL ID: an `i-...` where an IP address or a DNS name
25675
+ * belongs, a `vpc-...` in a security-group slot, a distribution id where a
25676
+ * hostname belongs, a proxy name in a `VpcId` position. Loud only where AWS
25677
+ * rejects the shape; an Output, an export or a free-text property carried it
25678
+ * silently. The physical id can never be the value for these attributes,
25679
+ * so the honest answer is a refusal naming the resource, the attribute, what
25680
+ * was observed and the remedy.
25681
+ *
25682
+ * DELIBERATELY NOT `markNonRetryable`, unlike every other refusal in this
25683
+ * file but the fabricated-account guard: the verdict is TIME-DEPENDENT. A
25684
+ * `pending` instance settles seconds later, and a failed describe can
25685
+ * succeed on the next attempt, so cdkd must not DECLARE it terminal — the
25686
+ * marker is that declaration, and `withRetry` honours it ahead of every
25687
+ * classifier. Leaving it off promises no retry: `isRetryableTransientError`
25688
+ * reads throttle names and HTTP statuses off the `cause` chain — which this
25689
+ * refusal deliberately does not carry, see below — and everything else by
25690
+ * message substring, and this message matches nothing on its own. So the
25691
+ * nested-stack replay (`NestedStackProvider.create` runs the child deploy
25692
+ * inside the parent's `withRetry`) re-runs the child only when an
25693
+ * interpolated hole happens to carry a pattern — the `logicalId` (the #1838
25694
+ * hazard: `DependencyViolation` and two other bare words), or the error
25695
+ * CLASS name `describeFailureObserved` puts in `observed`, which none of
25696
+ * these three reads raises — and then re-describes and can heal.
25697
+ * Measured, not designed: the fabricated-account guard sits in exactly the
25698
+ * same place. Threading a sanitized SDK error as `cause` so a throttled
25699
+ * describe classifies as transient was considered and left out — a
25700
+ * `CdkdError`'s `cause` is rendered at default verbosity by `formatError`
25701
+ * (`Caused by:`), so the clone would need its own fence. What the
25702
+ * refusal does at each consumer: a resource property fails the resource; a
25703
+ * stack Output is caught per-output by `DeployEngine.
25704
+ * handleOutputResolutionFailure`, skipped and recorded in `skippedOutputs`
25705
+ * (#2740), then re-resolved on the next deploy; an `Fn::Sub` re-raises it
25706
+ * (#1740); a `Conditions` entry still absorbs it to `false`
25707
+ * (`evaluateConditions`).
25708
+ *
25709
+ * `observed` and `remedy` are cdkd-authored sentences built by the caller
25710
+ * from an instance STATE name, an error CLASS name or a fixed phrase — never
25711
+ * AWS's raw message, which `describeFailureObserved` keeps at debug (the
25712
+ * per-reader rule in `.claude/rules/provider-resource-identity.md`: a
25713
+ * denied describe quotes the caller's account, role and session).
25714
+ */
25715
+ refuseUnservedAttribute(site) {
25716
+ const { logicalId, attributeName, resourceType, physicalId, context, observed, remedy } = site;
25717
+ throw new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resourceType}: ${observed}. The physical id "${displaySafe(this.maskThenStripThenMask(physicalId, context))}" is not a usable ${this.maskSecretsForLog(attributeName, context)}, so cdkd refuses to substitute it. ${remedy}`);
25718
+ }
25719
+ /**
25720
+ * The `observed` clause of {@link refuseUnservedAttribute} for a live read
25721
+ * that THREW: names the read and the error's CLASS at default verbosity and
25722
+ * puts AWS's own text behind `--verbose`, since a denied describe quotes the
25723
+ * caller's account, role and session into its message.
25724
+ */
25725
+ describeFailureObserved(read, err, context) {
25726
+ const name = displaySafe(err instanceof Error && err.name ? err.name : "Error", { asciiOnly: true }).slice(0, 64).trim() || "Error";
25727
+ const status = err?.$metadata?.httpStatusCode;
25728
+ const errorClass = typeof status === "number" && Number.isInteger(status) ? `${name}, HTTP ${status}` : name;
25729
+ this.logger.debug(`${read} failed (${errorClass}): ${this.maskSecretsForLog(err instanceof Error ? err.message : String(err), context)}`);
25730
+ return `${read} failed (${errorClass}); re-run with --verbose for the AWS error text`;
25731
+ }
25732
+ /**
25473
25733
  * Shared unknown-attribute physicalId fallback (issues #1106 / #1111).
25474
25734
  *
25475
25735
  * Applied by BOTH the final unknown-type fallback of
@@ -25501,9 +25761,9 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
25501
25761
  const expectsUrlShape = attributeName.endsWith("Url") && !/^https?:\/\//.test(physicalId);
25502
25762
  if (expectsArnShape || expectsUrlShape) {
25503
25763
  const expectedShape = expectsArnShape ? "an ARN (arn:...)" : "a URL (http(s)://...)";
25504
- throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resourceType}: attributes are not enriched for this resource type, and the physical ID fallback "${this.maskSecretsForLog(physicalId, context)}" is not ${expectedShape}. CloudFormation would return a different value here, so falling back to the physical ID would silently produce a wrong value (e.g. in stack Outputs). Avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${this.maskSecretsForLog(attributeName, context)}.`));
25764
+ throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resourceType}: attributes are not enriched for this resource type, and the physical ID fallback "${displaySafe(this.maskThenStripThenMask(physicalId, context))}" is not ${expectedShape}. CloudFormation would return a different value here, so falling back to the physical ID would silently produce a wrong value (e.g. in stack Outputs). Avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${this.maskSecretsForLog(attributeName, context)}.`));
25505
25765
  }
25506
- if (this.strictGetAtt) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resourceType}: attributes are not enriched for this resource type, and --strict-getatt rejects the physical ID fallback "${this.maskSecretsForLog(physicalId, context)}" (which may not be the value CloudFormation would return). Drop --strict-getatt to fall back with a warning, avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${this.maskSecretsForLog(attributeName, context)}.`));
25766
+ if (this.strictGetAtt) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${logicalId}, ${this.maskSecretsForLog(attributeName, context)}] for ${resourceType}: attributes are not enriched for this resource type, and --strict-getatt rejects the physical ID fallback "${displaySafe(this.maskThenStripThenMask(physicalId, context))}" (which may not be the value CloudFormation would return). Drop --strict-getatt to fall back with a warning, avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${this.maskSecretsForLog(attributeName, context)}.`));
25507
25767
  this.physicalIdFallbackCount++;
25508
25768
  this.logger.warn(`Unknown attribute ${this.maskSecretsForLog(attributeName, context)} for resource type ${resourceType}, returning physical ID`);
25509
25769
  return physicalId;
@@ -25518,12 +25778,36 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
25518
25778
  let values = rawValues;
25519
25779
  if (!Array.isArray(values)) values = await this.resolveValue(values, context);
25520
25780
  if (!Array.isArray(values)) throw new Error(`Fn::Join's second argument must be a list (an array literal or a list-returning intrinsic such as Fn::Cidr / Fn::GetAZs / Fn::Split / a Ref to a list-typed parameter — any List<...> type or CommaDelimitedList), but resolved to ${typeof values}`);
25521
- let result = (await allSettledKeepingFirstRejection(values.map(async (v) => {
25522
- const resolved = await this.resolveValue(v, context);
25523
- return String(resolved);
25524
- }), (pending) => this.warnAbandonedParts(pending))).join(delimiter);
25525
- if (result.includes("{{resolve:")) result = await this.resolveDynamicReferences(result, context);
25526
- this.logger.debug(`Resolved Fn::Join: ${this.maskSecretsForLog(result, context)}`);
25781
+ const literalList = Array.isArray(rawValues);
25782
+ const parts = (await allSettledKeepingFirstRejection(values.map(async (v) => {
25783
+ if (typeof v === "string") return {
25784
+ ...v.includes("{{resolve:") ? await this.resolveDynamicReferencesWithLogTwin(v, v, context) : {
25785
+ result: v,
25786
+ twin: v
25787
+ },
25788
+ product: !literalList
25789
+ };
25790
+ const raw = await this.resolveValue(v, context);
25791
+ const resolved = String(raw);
25792
+ return {
25793
+ result: resolved,
25794
+ twin: resolved,
25795
+ product: true,
25796
+ raw: { value: raw }
25797
+ };
25798
+ }), (pending) => this.warnAbandonedParts(pending))).map((part) => {
25799
+ if (!part.product) return part;
25800
+ if (part.raw) return {
25801
+ result: part.result,
25802
+ twin: this.productLogTwin(part.raw.value, context)
25803
+ };
25804
+ return this.logTwinOfProduct(part, context);
25805
+ });
25806
+ let result = parts.map((part) => part.result).join(delimiter);
25807
+ let twin = parts.map((part) => part.twin).join(delimiter);
25808
+ if (result.includes("{{resolve:")) ({result, twin} = await this.resolveDynamicReferencesWithLogTwin(result, twin, context));
25809
+ this.rememberLogTwin(context, result, twin);
25810
+ this.logger.debug(`Resolved Fn::Join: ${this.maskSecretsForLog(this.logTwinText(result, twin, context), context)}`);
25527
25811
  return result;
25528
25812
  }
25529
25813
  /**
@@ -25666,11 +25950,19 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
25666
25950
  async resolveSub(subArgs, context) {
25667
25951
  let template;
25668
25952
  const variables = Object.create(null);
25953
+ const variableTwins = Object.create(null);
25669
25954
  if (Array.isArray(subArgs)) {
25670
25955
  const [templateString, variableMap] = subArgs;
25671
25956
  template = templateString;
25672
25957
  if (typeof variableMap !== "object" || variableMap === null) throw markNonRetryable(/* @__PURE__ */ new Error(`Fn::Sub: the second element must be a variable map, got ${variableMap === null ? "null" : typeof variableMap}`));
25673
- for (const [key, val] of Object.entries(variableMap)) variables[key] = await this.resolveValue(val, context);
25958
+ for (const [key, val] of Object.entries(variableMap)) if (typeof val === "string") {
25959
+ const resolved = val.includes("{{resolve:") ? await this.resolveDynamicReferencesWithLogTwin(val, val, context) : {
25960
+ result: val,
25961
+ twin: val
25962
+ };
25963
+ variables[key] = resolved.result;
25964
+ variableTwins[key] = resolved.twin;
25965
+ } else variables[key] = await this.resolveValue(val, context);
25674
25966
  } else template = subArgs;
25675
25967
  const replacements = [];
25676
25968
  const matches = template.matchAll(/\$\{(!)?([^}]*)\}/g);
@@ -25678,31 +25970,39 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
25678
25970
  const isEscaped = match[1] === "!";
25679
25971
  const varNameStr = match[2];
25680
25972
  if (isEscaped) {
25973
+ const escapedLiteral = `\${${varNameStr ?? ""}}`;
25681
25974
  replacements.push({
25682
25975
  match: match[0],
25683
- replacement: `\${${varNameStr ?? ""}}`
25976
+ replacement: escapedLiteral,
25977
+ twin: escapedLiteral
25684
25978
  });
25685
25979
  continue;
25686
25980
  }
25687
25981
  if (!varNameStr) {
25688
25982
  replacements.push({
25689
25983
  match: match[0],
25690
- replacement: match[0]
25984
+ replacement: match[0],
25985
+ twin: match[0]
25691
25986
  });
25692
25987
  continue;
25693
25988
  }
25694
25989
  let replacement;
25695
- if (varNameStr in variables) replacement = String(variables[varNameStr]);
25696
- else {
25990
+ let twinReplacement;
25991
+ if (varNameStr in variables) {
25992
+ replacement = String(variables[varNameStr]);
25993
+ twinReplacement = varNameStr in variableTwins ? variableTwins[varNameStr] : this.productLogTwin(variables[varNameStr], context);
25994
+ } else {
25697
25995
  const pseudoValue = await this.resolvePseudoParameter(varNameStr, context);
25698
25996
  if (pseudoValue !== void 0) replacement = String(pseudoValue);
25699
25997
  else try {
25700
25998
  const value = await this.resolveRef(varNameStr, context);
25701
25999
  replacement = String(value);
26000
+ twinReplacement = this.productLogTwin(value, context);
25702
26001
  } catch (refError) {
25703
26002
  if (varNameStr.includes(".")) try {
25704
26003
  const value = await this.resolveGetAtt(varNameStr, context);
25705
26004
  replacement = String(value);
26005
+ twinReplacement = this.productLogTwin(value, context);
25706
26006
  } catch (getAttError) {
25707
26007
  if (getAttError instanceof IntrinsicResolutionRefusalError) throw getAttError;
25708
26008
  this.rethrowStructuralSubFailure(varNameStr, getAttError, context);
@@ -25719,7 +26019,8 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
25719
26019
  }
25720
26020
  replacements.push({
25721
26021
  match: match[0],
25722
- replacement
26022
+ replacement,
26023
+ twin: twinReplacement ?? replacement
25723
26024
  });
25724
26025
  }
25725
26026
  let cursor = 0;
@@ -25727,8 +26028,14 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
25727
26028
  const entry = replacements[cursor++];
25728
26029
  return entry ? entry.replacement : whole;
25729
26030
  });
25730
- if (result.includes("{{resolve:")) result = await this.resolveDynamicReferences(result, context);
25731
- this.logger.debug(`Resolved Fn::Sub: ${this.maskSecretsForLog(result, context)}`);
26031
+ let twinCursor = 0;
26032
+ let twin = template.replace(/\$\{(!)?([^}]*)\}/g, (whole) => {
26033
+ const entry = replacements[twinCursor++];
26034
+ return entry ? entry.twin : whole;
26035
+ });
26036
+ if (result.includes("{{resolve:")) ({result, twin} = await this.resolveDynamicReferencesWithLogTwin(result, twin, context));
26037
+ this.rememberLogTwin(context, result, twin);
26038
+ this.logger.debug(`Resolved Fn::Sub: ${this.maskSecretsForLog(this.logTwinText(result, twin, context), context)}`);
25732
26039
  return result;
25733
26040
  }
25734
26041
  /**
@@ -25890,7 +26197,8 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
25890
26197
  throw markNonRetryable(new IntrinsicResolutionRefusalError(`Fn::Split: the value to split${sourceClause} must be a string, got ${resolvedValue === null ? "null" : typeof resolvedValue}. Fn::Split accepts only a string; check the value or the intrinsic that produced it.`));
25891
26198
  }
25892
26199
  const result = resolvedValue.split(delimiter);
25893
- this.logger.debug(`Resolved Fn::Split: split by "${delimiter}" -> ${JSON.stringify(this.maskValueLeaves(result, context))}`);
26200
+ const pieceTwins = this.splitLogTwins(resolvedValue, delimiter, result, context);
26201
+ this.logger.debug(`Resolved Fn::Split: split by "${delimiter}" -> ${JSON.stringify(this.maskValueLeaves(pieceTwins, context))}`);
25894
26202
  return result;
25895
26203
  }
25896
26204
  /**
@@ -26651,8 +26959,9 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
26651
26959
  const resolvedValue = await this.resolveValue(value, context);
26652
26960
  if (typeof resolvedValue !== "string") throw new Error(`Fn::Base64: value must resolve to a string, got ${typeof resolvedValue}`);
26653
26961
  const result = Buffer.from(resolvedValue).toString("base64");
26654
- if (context.recordedSecretValues && this.maskSecretsForLog(resolvedValue, context) !== resolvedValue) recordMaskOnlyValue(context.recordedSecretValues, result);
26655
- this.logger.debug(`Resolved Fn::Base64: ${this.maskSecretsForLog(resolvedValue, context)} -> ${this.maskSecretsForLog(result, context)}`);
26962
+ const inputLogText = this.logTextOfLeaf(resolvedValue, context);
26963
+ if (context.recordedSecretValues && (inputLogText !== resolvedValue || this.maskSecretsForLog(resolvedValue, context) !== resolvedValue)) recordMaskOnlyValue(context.recordedSecretValues, result);
26964
+ this.logger.debug(`Resolved Fn::Base64: ${this.maskSecretsForLog(inputLogText, context)} -> ${this.maskSecretsForLog(inputLogText !== resolvedValue ? "***" : result, context)}`);
26656
26965
  return result;
26657
26966
  }
26658
26967
  /**
@@ -26757,6 +27066,155 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
26757
27066
  return masked;
26758
27067
  }
26759
27068
  /**
27069
+ * Is `value` WHOLE a secret either bag holds (issue #3100)? Asked of a
27070
+ * resolution PRODUCT only — never of a template literal — so an exact match
27071
+ * at any length is a verdict about what was written, not a floorless needle.
27072
+ */
27073
+ isRecordedSecretForLog(value, context) {
27074
+ return context?.recordedSecretValues?.has(value) === true || context?.inheritedSecrets?.has(value) === true;
27075
+ }
27076
+ /**
27077
+ * The text a `Resolved Fn::Join:` / `Resolved Fn::Sub:` line prints (issue
27078
+ * #3100): the twin through the needle mask — unless the needle mask ALSO
27079
+ * fires on the value itself, in which case the whole line is masked. The
27080
+ * twin splits the text at the spans it masked, so a 4+ character recorded
27081
+ * secret that overlaps one of them (or IS the whole value) is no longer a
27082
+ * contiguous needle in the twin, and masking the twin alone would print the
27083
+ * part of it outside the span. The value's own needle mask cannot be merged
27084
+ * with the twin's positions, so the line gives both up for `***`: never
27085
+ * more text than either mask alone would print. Returns the text BEFORE the
27086
+ * needle mask; each log line wraps it in `maskSecretsForLog` itself, so the
27087
+ * resolver's mask-coverage checker sees a masker at the site.
27088
+ */
27089
+ logTwinText(result, twin, context) {
27090
+ return twin !== result && this.maskSecretsForLog(result, context) !== result ? "***" : twin;
27091
+ }
27092
+ /**
27093
+ * The log twins of `Fn::Split`'s pieces (issue #3100), each registered for
27094
+ * the pass so an outer `Fn::Join` over the pieces keeps the mask. A source
27095
+ * that is itself a recorded or inherited secret counts as a twin of `***`;
27096
+ * a source with neither is its pieces' own twin.
27097
+ *
27098
+ * The source's twin is split by the same delimiter. When both splits give
27099
+ * the same number of pieces they are paired by position; when they do not —
27100
+ * a secret carrying the delimiter, or a delimiter that occurs in the mask
27101
+ * itself — no pairing can be trusted, so every piece is masked whole. Every
27102
+ * masked piece is registered by VALUE, so an equal string elsewhere in the
27103
+ * pass is masked too: the over-masking direction `logTwinOfProduct` accepts.
27104
+ */
27105
+ splitLogTwins(value, delimiter, pieces, context) {
27106
+ const bag = this.logTwinBag(context);
27107
+ const sourceTwin = this.isRecordedSecretForLog(value, context) ? "***" : bag ? LOG_TWINS_BY_PASS.get(bag)?.get(value) : void 0;
27108
+ if (sourceTwin === void 0) return pieces;
27109
+ const twinPieces = sourceTwin.split(delimiter);
27110
+ const aligned = twinPieces.length === pieces.length && !"***".includes(delimiter);
27111
+ return pieces.map((piece, index) => {
27112
+ const twin = this.logTwinText(piece, aligned ? twinPieces[index] ?? "***" : "***", context);
27113
+ this.rememberLogTwin(context, piece, twin);
27114
+ return twin;
27115
+ });
27116
+ }
27117
+ /**
27118
+ * The log twin of a resolution product as `String(value)` renders it (issue
27119
+ * #3100). A LIST is stringified element by element exactly as
27120
+ * `Array.prototype.join` does (`null` / `undefined` as the empty string,
27121
+ * nested lists recursively, comma-separated), so each element keeps its own
27122
+ * twin: `String()` over the whole list produced a string no registered twin
27123
+ * matched. No cycle guard: a product is JSON-sourced state or a fresh
27124
+ * resolution result, and neither can hold a self-referencing list.
27125
+ */
27126
+ productLogTwin(value, context) {
27127
+ if (Array.isArray(value)) return value.map((element) => element === null || element === void 0 ? "" : this.productLogTwin(element, context)).join(",");
27128
+ const text = String(value);
27129
+ return this.logTwinOfProduct({
27130
+ result: text,
27131
+ twin: text
27132
+ }, context).twin;
27133
+ }
27134
+ /**
27135
+ * The pre-mask log text of a string LEAF a debug line prints (issue #3100):
27136
+ * the leaf treated as a resolution product — masked whole when it is a
27137
+ * recorded secret, otherwise the masked twin this pass registered for it —
27138
+ * and then `logTwinText`'s whole-line guard. The caller still wraps the
27139
+ * result in `maskSecretsForLog`. Shared by every caller of `maskValueLeaves`
27140
+ * (the `Fn::Select`, `Fn::Split`, `Fn::Equals`, `Fn::FindInMap`,
27141
+ * `Fn::GetAZs` and `Fn::Cidr` lines and the VPC `Ipv6CidrBlocks` line among
27142
+ * them) and by the `Fn::Base64` line, so a string one
27143
+ * write already masked by position is not printed in the clear by the next
27144
+ * intrinsic that logs it. `maskValueLeaves` also masks values interpolated
27145
+ * into THROWN messages (the `Fn::Cidr` argument refusals, for one), so those
27146
+ * messages take the same position mask: a short secret an earlier write put
27147
+ * into the value is `***` there too, the direction the mask at the throw
27148
+ * already takes for a whole secret.
27149
+ */
27150
+ logTextOfLeaf(value, context) {
27151
+ return this.logTwinText(value, this.logTwinOfProduct({
27152
+ result: value,
27153
+ twin: value
27154
+ }, context).twin, context);
27155
+ }
27156
+ /** The bag a pass's log twins are keyed by (issue #3100): the recorded one, else the inherited one. */
27157
+ logTwinBag(context) {
27158
+ return context?.recordedSecretValues ?? context?.inheritedSecrets;
27159
+ }
27160
+ /**
27161
+ * The log twin of a RESOLUTION PRODUCT — an intrinsic part, variable or
27162
+ * placeholder — for issue #3100. Masked whole when its value is a recorded
27163
+ * secret. Otherwise a part that carries no mask of its own takes the masked
27164
+ * twin an earlier write of this pass registered for that exact string
27165
+ * (`rememberLogTwin`) — how a Join part that is itself an `Fn::Sub`, or a
27166
+ * `Fn::Select` over a reference-bearing string, keeps the inner mask. A part
27167
+ * whose OWN masked twin disagrees with the registered one (a list element
27168
+ * still spelling a reference) is masked whole, since the two span sets
27169
+ * cannot be merged; one that agrees keeps its twin.
27170
+ *
27171
+ * The registry is keyed by VALUE, so a product equal to a registered string
27172
+ * from another provenance takes its mask too. That over-masks a line whose
27173
+ * text holds a recorded secret at a position some write in this pass put
27174
+ * one. The registry holds masked twins only, so a lookup never unmasks.
27175
+ *
27176
+ * The whole-secret check reads the bags as they stand when the product is
27177
+ * placed, before the enclosing Join / Sub runs its final substitution. A
27178
+ * secret that substitution records is therefore not matched against an
27179
+ * earlier product equal to it. Such a product did not come from that secret:
27180
+ * a parameter holding one arrives in the inherited bag before its `Ref`
27181
+ * resolves, so what stays printed is a public value that coincides with it.
27182
+ */
27183
+ logTwinOfProduct(part, context) {
27184
+ if (this.isRecordedSecretForLog(part.result, context)) return {
27185
+ result: part.result,
27186
+ twin: "***"
27187
+ };
27188
+ const bag = this.logTwinBag(context);
27189
+ const registered = bag ? LOG_TWINS_BY_PASS.get(bag)?.get(part.result) : void 0;
27190
+ if (registered === void 0 || registered === part.twin) return part;
27191
+ return {
27192
+ result: part.result,
27193
+ twin: part.twin === part.result ? registered : "***"
27194
+ };
27195
+ }
27196
+ /**
27197
+ * Register a MASKED `twin` as the log twin of `result` for this pass (issue
27198
+ * #3100). An unmasked twin is not registered, so a later literal that
27199
+ * resolves to the same string cannot replace an earlier write's mask, and
27200
+ * two DIFFERENT masked twins for one string register `***` for the whole
27201
+ * string, since their spans cannot be merged. Scoped to the pass's bag
27202
+ * through a `WeakMap`, like the cross-stack associations, so it dies with
27203
+ * the bag and another pass's strings cannot be reached.
27204
+ */
27205
+ rememberLogTwin(context, result, twin) {
27206
+ if (twin === result) return;
27207
+ const bag = this.logTwinBag(context);
27208
+ if (!bag) return;
27209
+ let twins = LOG_TWINS_BY_PASS.get(bag);
27210
+ if (!twins) {
27211
+ twins = /* @__PURE__ */ new Map();
27212
+ LOG_TWINS_BY_PASS.set(bag, twins);
27213
+ }
27214
+ const existing = twins.get(result);
27215
+ twins.set(result, existing === void 0 || existing === twin ? twin : "***");
27216
+ }
27217
+ /**
26760
27218
  * A copy of `value` with every string LEAF (and every object KEY) masked, for
26761
27219
  * a caller about to ENCODE it into a message (issue
26762
27220
  * [#2759](https://github.com/go-to-k/cdkd/issues/2759)).
@@ -26788,7 +27246,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
26788
27246
  maskValueLeaves(value, context) {
26789
27247
  const done = /* @__PURE__ */ new Map();
26790
27248
  const walk = (node) => {
26791
- if (typeof node === "string") return this.maskSecretsForLog(node, context);
27249
+ if (typeof node === "string") return this.maskSecretsForLog(this.logTextOfLeaf(node, context), context);
26792
27250
  if (node === null || typeof node !== "object") return node;
26793
27251
  const memo = done.get(node);
26794
27252
  if (memo !== void 0) return memo;
@@ -26800,7 +27258,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
26800
27258
  }
26801
27259
  const out = Object.create(null);
26802
27260
  done.set(node, out);
26803
- for (const [key, child] of Object.entries(node)) out[this.maskSecretsForLog(key, context)] = walk(child);
27261
+ for (const [key, child] of Object.entries(node)) out[this.maskSecretsForLog(this.logTextOfLeaf(key, context), context)] = walk(child);
26804
27262
  return out;
26805
27263
  };
26806
27264
  return walk(value);
@@ -26846,8 +27304,32 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
26846
27304
  return this.maskSecretsForLog(stripControlChars(this.maskSecretsForLog(value, context)), context);
26847
27305
  }
26848
27306
  async resolveDynamicReferences(value, context) {
27307
+ return (await this.resolveDynamicReferencesWithLogTwin(value, value, context)).result;
27308
+ }
27309
+ /**
27310
+ * {@link resolveDynamicReferences}, also returning the LOG TWIN of the result
27311
+ * (issue [#3100](https://github.com/go-to-k/cdkd/issues/3100), see
27312
+ * `LogTwin`). `logTwin` is the caller's twin of `value`, which may already
27313
+ * mask spans an earlier write put there.
27314
+ *
27315
+ * Every arm that writes into `result` writes into the twin too, replacing
27316
+ * the SAME token: a secret verdict writes {@link SECRET_MASK}, anything else
27317
+ * the value itself. The verdict is the one each arm already holds — the
27318
+ * fresh lookup's `isSecret`, the cache entry's own `secret` (never the
27319
+ * process-wide store, for the #1933 reason the cache-hit arm states), and
27320
+ * for a region-pinned sibling the twin the sibling itself returns. An arm
27321
+ * that `continue`s without writing leaves the token in both.
27322
+ *
27323
+ * A token whose text an earlier write masked (`{{resolve:${Pw}}}` over a
27324
+ * secret `Pw`) is not found in the twin, so the twin keeps the masked token
27325
+ * where the value holds its resolution. What the twin cannot carry is a
27326
+ * NEEDLE match that straddles a masked span, which is why the log lines go
27327
+ * through `logTwinText` rather than masking the twin alone.
27328
+ */
27329
+ async resolveDynamicReferencesWithLogTwin(value, logTwin, context) {
26849
27330
  const pattern = /\{\{resolve:([^}]+)\}\}/g;
26850
27331
  let result = value;
27332
+ let twin = logTwin;
26851
27333
  let match;
26852
27334
  const matches = [];
26853
27335
  while ((match = pattern.exec(value)) !== null) matches.push({
@@ -26861,8 +27343,9 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
26861
27343
  const regionVerdict = classifyReplaySecretRegion(fullMatch, this.explicitRegion ?? this.resolverRegion, context?.producerRegions);
26862
27344
  if (regionVerdict.kind === "ambiguous") throw markNonRetryable(new DynamicReferenceRegionAmbiguousError(`Refusing to resolve the secret reference ${this.maskSecretsForLog(fullMatch, context)}: it names '${this.maskSecretsForLog(regionVerdict.secretName, context)}' without a region, and this stack reads from ${regionVerdict.foreignProducerRegions.join(", ")} as well as its own region. cdkd cannot tell which one must answer, and resolving against the wrong one yields a different secret. Spell the reference as a full ARN to say which region owns it.`));
26863
27345
  if (regionVerdict.kind === "named-region") {
26864
- const foreign = await this.resolverForProducerRegion(regionVerdict.region).resolveDynamicReferences(fullMatch, withoutProducerRegions(context));
26865
- result = result.replace(fullMatch, () => foreign);
27346
+ const foreign = await this.resolverForProducerRegion(regionVerdict.region).resolveDynamicReferencesWithLogTwin(fullMatch, fullMatch, withoutProducerRegions(context));
27347
+ result = result.replace(fullMatch, () => foreign.result);
27348
+ twin = twin.replace(fullMatch, () => foreign.twin);
26866
27349
  continue;
26867
27350
  }
26868
27351
  const cached = this.cachedDynamicReferences.get(fullMatch);
@@ -26872,6 +27355,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
26872
27355
  if (context?.recordedSecretValues) recordResolvedPair(context.recordedSecretValues, fullMatch, cached.value);
26873
27356
  }
26874
27357
  result = result.replace(fullMatch, () => cached.value);
27358
+ twin = twin.replace(fullMatch, () => cached.secret && cached.value ? "***" : cached.value);
26875
27359
  continue;
26876
27360
  }
26877
27361
  const parts = inner.split(":");
@@ -26921,8 +27405,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
26921
27405
  if (service === "secretsmanager" || service === "ssm-secure") this.pinSecretVerdict(fullMatch, true);
26922
27406
  }
26923
27407
  result = result.replace(fullMatch, () => resolved);
27408
+ twin = twin.replace(fullMatch, () => isSecret && resolved ? "***" : resolved);
26924
27409
  }
26925
- return result;
27410
+ if (context) this.rememberLogTwin(context, result, twin);
27411
+ return {
27412
+ result,
27413
+ twin
27414
+ };
26926
27415
  }
26927
27416
  /**
26928
27417
  * Resolve a Secrets Manager dynamic reference
@@ -28486,7 +28975,7 @@ var CloudControlProvider = class {
28486
28975
  const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
28487
28976
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
28488
28977
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
28489
- const { ASGProvider } = await import("./asg-provider-BsC_XndT.js").then((n) => n.n);
28978
+ const { ASGProvider } = await import("./asg-provider-RLnpBIKj.js").then((n) => n.n);
28490
28979
  const asgProvider = new ASGProvider();
28491
28980
  return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
28492
28981
  }
@@ -33684,12 +34173,20 @@ const WARM_THROUGHPUT_MEMBERS = ["ReadUnitsPerSecond", "WriteUnitsPerSecond"];
33684
34173
  *
33685
34174
  * The accepted STRING set is `Number()`'s, which is WIDER than a decimal
33686
34175
  * integer: `'0x1e'`, `'0o36'`, `'1e3'`, `'30.5'` and `' 30 '` all coerce.
33687
- * Whether CloudFormation accepts those for an `Integer`-typed property is
33688
- * unmeasured, and issue [#2698](https://github.com/go-to-k/cdkd/issues/2698)
33689
- * holds the live A/B that would settle it. It matters at the CALLERS that
33690
- * FORWARD the result to AWS rather than merely compare it — narrowing this
33691
- * shared helper would change the DynamoDB capacity readers too, so read that
33692
- * issue before tightening anything here.
34176
+ * CloudFormation accepts only the LAST of those for an `Integer`-typed
34177
+ * property — measured on `AWS::Logs::LogGroup.RetentionInDays`, issue
34178
+ * [#2698](https://github.com/go-to-k/cdkd/issues/2698) — and
34179
+ * {@link toCfnInteger} is the reader that matches that measurement. It matters
34180
+ * at the CALLERS that FORWARD the result to AWS rather than merely compare it:
34181
+ * the log-group provider switched; the DynamoDB capacity / throughput
34182
+ * forwarders (`coerceWarmThroughput` below, and the `GlobalTable` provider's
34183
+ * `ProvisionedThroughput` / `OnDemandThroughput` readers) still read through
34184
+ * THIS helper, because the measurement covers one resource handler and
34185
+ * DynamoDB's has not been A/B'd — issue
34186
+ * [#3135](https://github.com/go-to-k/cdkd/issues/3135) holds that pass. A
34187
+ * GUARD caller (`stateful-types.ts`'s `has-retention`, the DynamoDB
34188
+ * already-matches / decrease tests) is safe on the wider set: over-acceptance
34189
+ * there can only produce MORE refusals or skip fewer calls.
33693
34190
  *
33694
34191
  * EXPORTED, and not because warm throughput needs it exported. This exact rule
33695
34192
  * was hand-written three times — here, as `dynamodb-table-provider.ts`'s
@@ -33710,6 +34207,59 @@ function toFiniteNumber(value) {
33710
34207
  }
33711
34208
  }
33712
34209
  /**
34210
+ * A CloudFormation-borne `Integer` property, read the way CloudFormation reads
34211
+ * it, or `undefined` when CloudFormation would REJECT the value.
34212
+ *
34213
+ * The sibling of {@link toFiniteNumber} for the callers that FORWARD the number
34214
+ * to AWS. That helper is `Number()`, which accepts hex, octal, binary,
34215
+ * exponent and decimal-point spellings; CloudFormation does not, and a
34216
+ * provider forwarding `Number('0x1e')` sends `30` for a template CloudFormation
34217
+ * would have refused outright. Measured live on
34218
+ * `AWS::Logs::LogGroup.RetentionInDays` (us-east-1, 2026-09-14, issue
34219
+ * [#2698](https://github.com/go-to-k/cdkd/issues/2698); a one-resource stack
34220
+ * updated per spelling, live value read back with `logs describe-log-groups`):
34221
+ *
34222
+ * ```
34223
+ * template spelling CloudFormation live retention after
34224
+ * 30 (JSON number) accepted 30
34225
+ * "60" accepted 60
34226
+ * "+30" accepted 30
34227
+ * " 30 " (padded) accepted 30
34228
+ * " " (whitespace only) accepted, treated as ABSENT none (policy REMOVED)
34229
+ * "" (empty string) accepted, treated as ABSENT none (policy REMOVED)
34230
+ * property absent accepted none (policy REMOVED)
34231
+ * "0x1e" REJECTED (handler) unchanged
34232
+ * "1e3" REJECTED (handler) unchanged
34233
+ * "30.5" REJECTED (handler) unchanged
34234
+ * "30.0" REJECTED (handler) unchanged
34235
+ * "abc" REJECTED (handler) unchanged
34236
+ * true / false REJECTED (handler) unchanged
34237
+ * 0 / "0" REJECTED (handler: enum) unchanged
34238
+ * null REJECTED at update-stack unchanged
34239
+ * ```
34240
+ *
34241
+ * So the accepted string grammar is `optional sign + decimal digits`, with
34242
+ * surrounding whitespace TRIMMED — `/^\s*[+-]?\d+\s*$/`, then `Number()`. The
34243
+ * digits grammar is `config-shape.ts`'s {@link coerceCfnInteger}, reused rather
34244
+ * than respelled so the two cannot drift on it; the ONE difference is the
34245
+ * trim, and it is a measured difference, not a relaxation: that helper's own
34246
+ * measurement (a NESTED `GenerateSecretString.PasswordLength`, 2026-09-13)
34247
+ * saw `" 12 "` REJECTED where this one saw `" 30 "` accepted. Per-handler
34248
+ * behaviour varies on padding, so each helper carries its own measurement and
34249
+ * a caller picks by the property it forwards.
34250
+ *
34251
+ * What this helper does NOT decide: the empty / whitespace-only strings above
34252
+ * are CloudFormation's spelling of "property absent", which is a statement
34253
+ * about the PROPERTY, not about the number — they answer `undefined` here, and
34254
+ * the caller decides whether `undefined` means "absent" or "unusable" from the
34255
+ * raw value (`logs-loggroup-provider.ts`'s `assertUsableRetention` is the
34256
+ * reference). `0` / `'0'` parse to `0` here; whether zero is usable is again the
34257
+ * property's question (the log-group handler rejects it by enum).
34258
+ */
34259
+ function toCfnInteger(value) {
34260
+ return coerceCfnInteger(typeof value === "string" ? value.trim() : value);
34261
+ }
34262
+ /**
33713
34263
  * Coerce a CFn `WarmThroughput` block to the numeric shape the SDK's `Long`
33714
34264
  * fields accept, PER MEMBER.
33715
34265
  *
@@ -36008,6 +36558,45 @@ function effectiveProvisionedBy(record, fallbackProvisionedBy) {
36008
36558
  return record?.provisionedBy ?? fallbackProvisionedBy;
36009
36559
  }
36010
36560
  /**
36561
+ * One spelling of "this value came from a rollback-journal record, and is
36562
+ * about to be interpolated into a message a terminal will render" (issue
36563
+ * #3092). The journal is a sibling of `state.json` in the same bucket,
36564
+ * writable by anyone with `s3:PutObject` and validated no more than an
36565
+ * unchecked cast; cdkd's output is line-oriented, so an injected newline
36566
+ * invents a line that reads like a real one. This executor runs under the
36567
+ * standalone `cdkd rollback` AND under the deploy engine's automatic
36568
+ * rollback, so its lines print on every failed deploy.
36569
+ *
36570
+ * `displayIdent`: the ASCII allowlist with `UNRENDERABLE` for a value that
36571
+ * sanitizes to nothing, a length cap, and a visible boundary (a JSON-quoted
36572
+ * rendering) for a value that is not a plain identifier -- the all-ASCII
36573
+ * `X (AWS::RDS::DBInstance) -- already reverted` the allowlist lets through.
36574
+ * A logical id, a CFn resource type and a change type all have known
36575
+ * charsets. Call it for those; `grep safe(` answers the scope.
36576
+ *
36577
+ * NOT for a value that is USED rather than shown -- the ids passed to a
36578
+ * provider call, the keys into `stateResources`, the `msg` a classifier reads.
36579
+ * NOT for the `reason` / `survivorReason` strings handed to `ctx.recordEvent`:
36580
+ * those are PERSISTED into `deployments/*.jsonl` raw and `cdkd events`
36581
+ * sanitizes them on the way out, so sanitizing here would put a display
36582
+ * transform on a stored value and double it at render (the go-to-k/cdkd#2170
36583
+ * direction). NOT for free-form error text either: an SDK message legitimately
36584
+ * carries non-ASCII, so a site rendering one calls `displaySafe()` directly and
36585
+ * takes the DENYLIST, as `formatError` does for a `cause`.
36586
+ */
36587
+ function safe(value) {
36588
+ return displayIdent(value);
36589
+ }
36590
+ /**
36591
+ * The one shape of `op.logicalId` this executor will print INSIDE a command it
36592
+ * invites the user to paste (`cdkd rollback --orphan <id>`): CloudFormation's
36593
+ * own logical-id charset and length. Stricter than "`safe()` is the identity
36594
+ * on it" on purpose -- `~user` and `=x` are plain identifiers the shell
36595
+ * expands before cdkd sees them (issue #3092 review). Not a display rule: a
36596
+ * legitimate id the executor merely SHOWS still goes through `safe()`.
36597
+ */
36598
+ const PASTEABLE_LOGICAL_ID = /^[A-Za-z0-9]{1,255}$/;
36599
+ /**
36011
36600
  * `UpdateReplacePolicy: Snapshot` on a rollback's delete-of-the-NEW-resource
36012
36601
  * (issue #1354): honor it where it costs nothing — an atomic-final-snapshot
36013
36602
  * type on the SDK route gets a generated identifier threaded into the delete
@@ -36104,7 +36693,7 @@ function rollbackRetainsNewResource(record) {
36104
36693
  */
36105
36694
  function retainedSurvivorMessages(logicalId, resourceType, survivorPhysicalId, stateClause) {
36106
36695
  return {
36107
- warn: ` ⚠ ${logicalId} (${resourceType}) has UpdateReplacePolicy: Retain — the replacement's new physical resource (${survivorPhysicalId}) is RETAINED by this rollback and is no longer tracked by cdkd: it keeps running and incurring cost, and \`cdkd destroy\` will not remove it. Delete it yourself once you no longer need it. ${stateClause}`,
36696
+ warn: ` ⚠ ${safe(logicalId)} (${safe(resourceType)}) has UpdateReplacePolicy: Retain — the replacement's new physical resource (${displaySafe(survivorPhysicalId)}) is RETAINED by this rollback and is no longer tracked by cdkd: it keeps running and incurring cost, and \`cdkd destroy\` will not remove it. Delete it yourself once you no longer need it. ${displaySafe(stateClause)}`,
36108
36697
  reason: `UpdateReplacePolicy: Retain kept the replacement's new ${resourceType} (${survivorPhysicalId}); it is live, still billing, and no longer tracked by cdkd. ${stateClause}`
36109
36698
  };
36110
36699
  }
@@ -36655,7 +37244,7 @@ function redactRollbackRecord(record, secrets, journaledProps) {
36655
37244
  }
36656
37245
  async function updateWithRollbackRetry(provider, args, logicalId, logger, isInterrupted, secrets) {
36657
37246
  if (provider.disableOuterRetry) return await withCurrentResourceSecrets(secrets, () => provider.update(...args));
36658
- return await withRetry(() => withCurrentResourceSecrets(secrets, () => provider.update(...args)), logicalId, {
37247
+ return await withRetry(() => withCurrentResourceSecrets(secrets, () => provider.update(...args)), safe(logicalId), {
36659
37248
  logger: maskingRetryLogger(logger, secrets),
36660
37249
  ...isInterrupted && {
36661
37250
  isInterrupted,
@@ -36747,13 +37336,14 @@ async function updateWithRollbackRetry(provider, args, logicalId, logger, isInte
36747
37336
  async function createWithRollbackRetry(provider, create, logicalId, logger, isInterrupted, secrets, outer) {
36748
37337
  if (provider.disableOuterRetry) return await create();
36749
37338
  const maskedLogger = maskingRetryLogger(logger, secrets);
36750
- return await withRetry(() => withRetry(create, logicalId, {
37339
+ const shownId = safe(logicalId);
37340
+ return await withRetry(() => withRetry(create, shownId, {
36751
37341
  logger: maskedLogger,
36752
37342
  ...isInterrupted && {
36753
37343
  isInterrupted,
36754
37344
  onInterrupted: () => /* @__PURE__ */ new Error("Rollback interrupted while retrying the replay re-create")
36755
37345
  }
36756
- }), logicalId, {
37346
+ }), shownId, {
36757
37347
  ...RECREATE_RETRY_SCHEDULE,
36758
37348
  logger: maskedLogger,
36759
37349
  ...isInterrupted && {
@@ -36836,18 +37426,18 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
36836
37426
  try {
36837
37427
  switch (action) {
36838
37428
  case "unrecoverable-delete":
36839
- logger.warn(` Rollback: Cannot restore deleted resource ${op.logicalId} (${op.resourceType}) — resource has already been deleted`);
37429
+ logger.warn(` Rollback: Cannot restore deleted resource ${safe(op.logicalId)} (${safe(op.resourceType)}) — resource has already been deleted`);
36840
37430
  result.warnings++;
36841
37431
  return;
36842
37432
  case "skip-already-done":
36843
- logger.debug(` Rollback: ${op.logicalId} already reverted, skipping`);
37433
+ logger.debug(` Rollback: ${safe(op.logicalId)} already reverted, skipping`);
36844
37434
  return;
36845
37435
  case "skip-mismatch":
36846
- logger.warn(` Rollback: Skipping ${op.logicalId} — its physical id changed since the failed deploy (replaced by a later attempt); manual attention may be required`);
37436
+ logger.warn(` Rollback: Skipping ${safe(op.logicalId)} — its physical id changed since the failed deploy (replaced by a later attempt); manual attention may be required`);
36847
37437
  result.warnings++;
36848
37438
  return;
36849
37439
  case "skip-absent":
36850
- logger.warn(` Rollback: Cannot restore ${op.logicalId} — resource no longer in state, skipping`);
37440
+ logger.warn(` Rollback: Cannot restore ${safe(op.logicalId)} — resource no longer in state, skipping`);
36851
37441
  result.warnings++;
36852
37442
  return;
36853
37443
  case "orphan-flag":
@@ -36856,7 +37446,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
36856
37446
  const orphanFlagProvisionedBy = effectiveProvisionedBy(record, op.provisionedBy);
36857
37447
  createRollbackRoute = orphanFlagProvisionedBy;
36858
37448
  delete stateResources[op.logicalId];
36859
- logger.info(` Rollback: Orphaning created resource ${op.logicalId} (--orphan)`);
37449
+ logger.info(` Rollback: Orphaning created resource ${safe(op.logicalId)} (--orphan)`);
36860
37450
  await afterOp?.(op.logicalId);
36861
37451
  ctx.recordEvent?.({
36862
37452
  eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
@@ -36870,7 +37460,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
36870
37460
  reason: `--orphan left ${op.logicalId} (${op.resourceType}) in AWS as ${record.physicalId} and dropped it from state; it is live, still billing, and no longer tracked by cdkd.`
36871
37461
  }
36872
37462
  });
36873
- } else logger.info(` Rollback: Leaving ${op.logicalId} at its new state (--orphan)`);
37463
+ } else logger.info(` Rollback: Leaving ${safe(op.logicalId)} at its new state (--orphan)`);
36874
37464
  return;
36875
37465
  case "orphan-retain": {
36876
37466
  const record = stateResources[op.logicalId];
@@ -36886,7 +37476,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
36886
37476
  onOrphan?.(orphaned);
36887
37477
  }
36888
37478
  delete stateResources[op.logicalId];
36889
- logger.info(` Rollback: Leaving ${op.logicalId} (${op.resourceType}) in AWS (DeletionPolicy: Retain) — removed from state`);
37479
+ logger.info(` Rollback: Leaving ${safe(op.logicalId)} (${safe(op.resourceType)}) in AWS (DeletionPolicy: Retain) — removed from state`);
36890
37480
  await afterOp?.(op.logicalId);
36891
37481
  ctx.recordEvent?.({
36892
37482
  eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
@@ -36905,7 +37495,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
36905
37495
  case "delete":
36906
37496
  case "delete-with-final-snapshot": {
36907
37497
  if (!op.physicalId) {
36908
- logger.warn(` Rollback: Cannot delete ${op.logicalId} — no physical ID recorded`);
37498
+ logger.warn(` Rollback: Cannot delete ${safe(op.logicalId)} — no physical ID recorded`);
36909
37499
  result.warnings++;
36910
37500
  return;
36911
37501
  }
@@ -36915,7 +37505,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
36915
37505
  const takeFinalSnapshot = snapshotPolicy && ctx.skipFinalSnapshot !== true;
36916
37506
  let finalSnapshotIdentifier;
36917
37507
  if (takeFinalSnapshot) finalSnapshotIdentifier = await prepareCreateRollbackFinalSnapshot(op, deleteProvisionedBy, ctx);
36918
- logger.info(` Rollback: Deleting created resource ${op.logicalId} (${op.resourceType})` + (takeFinalSnapshot ? " — DeletionPolicy: Snapshot" : "") + (snapshotPolicy && !takeFinalSnapshot ? " — DeletionPolicy: Snapshot NOT taken (--skip-final-snapshot)" : ""));
37508
+ logger.info(` Rollback: Deleting created resource ${safe(op.logicalId)} (${safe(op.resourceType)})` + (takeFinalSnapshot ? " — DeletionPolicy: Snapshot" : "") + (snapshotPolicy && !takeFinalSnapshot ? " — DeletionPolicy: Snapshot NOT taken (--skip-final-snapshot)" : ""));
36919
37509
  const { provider } = ctx.providerRegistry.getProviderFor({
36920
37510
  resourceType: op.resourceType,
36921
37511
  provisionedBy: deleteProvisionedBy
@@ -36925,7 +37515,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
36925
37515
  ...finalSnapshotIdentifier !== void 0 && { finalSnapshotIdentifier }
36926
37516
  }), op.logicalId, op.physicalId, "while rolling back its CREATE");
36927
37517
  delete stateResources[op.logicalId];
36928
- logger.info(` Rollback: ${op.logicalId} deleted successfully`);
37518
+ logger.info(` Rollback: ${safe(op.logicalId)} deleted successfully`);
36929
37519
  await afterOp?.(op.logicalId);
36930
37520
  ctx.recordEvent?.({
36931
37521
  eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
@@ -36940,7 +37530,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
36940
37530
  case "reverse-replacement-readopt": {
36941
37531
  const current = stateResources[op.logicalId];
36942
37532
  const prev = op.previousState;
36943
- logger.info(` Rollback: Reversing replacement of ${op.logicalId} (${op.resourceType}) — deleting the new resource and re-adopting the retained old one (${prev.physicalId})`);
37533
+ logger.info(` Rollback: Reversing replacement of ${safe(op.logicalId)} (${safe(op.resourceType)}) — deleting the new resource and re-adopting the retained old one (${displaySafe(prev.physicalId)})`);
36944
37534
  /**
36945
37535
  * Set when this arm ORPHANS the replacement's new copy. Read at the
36946
37536
  * `ROLLBACK_RESOURCE_SUCCEEDED` event below, which is the only channel
@@ -36974,7 +37564,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
36974
37564
  }), op.logicalId, current.physicalId, "while reversing its replacement (re-adopting the retained old resource)");
36975
37565
  }
36976
37566
  stateResources[op.logicalId] = prev;
36977
- logger.info(` Rollback: ${op.logicalId} restored to the retained old resource`);
37567
+ logger.info(` Rollback: ${safe(op.logicalId)} restored to the retained old resource`);
36978
37568
  await afterOp?.(op.logicalId);
36979
37569
  const survivorProvisionedBy = current.provisionedBy;
36980
37570
  ctx.recordEvent?.({
@@ -36998,8 +37588,8 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
36998
37588
  const resolvedPrevProps = await resolveReplayProps(prev.properties, resolver, secrets, ctx, op.logicalId) ?? {};
36999
37589
  refuseMaskedReplayBaseline(resolvedPrevProps, op.logicalId);
37000
37590
  recordNestedStackParameterExpressions(secrets, op.resourceType, resolvedPrevProps, prev.properties, STATE_DERIVED_RULES);
37001
- logger.info(` Rollback: Reversing replacement of ${op.logicalId} (${op.resourceType}) — re-creating the old resource and deleting the new one`);
37002
- 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.`);
37591
+ logger.info(` Rollback: Reversing replacement of ${safe(op.logicalId)} (${safe(op.resourceType)}) — re-creating the old resource and deleting the new one`);
37592
+ if (STATEFUL_TYPES.has(op.resourceType)) logger.warn(` ⚠ ${safe(op.logicalId)} (${safe(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.`);
37003
37593
  const { provider: createProvider } = ctx.providerRegistry.getProviderFor({
37004
37594
  resourceType: op.resourceType,
37005
37595
  provisionedBy: prev.provisionedBy
@@ -37018,8 +37608,8 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
37018
37608
  } catch (createError) {
37019
37609
  const msg = createError instanceof Error ? createError.message : String(createError);
37020
37610
  if (!isNameCollisionError(msg)) throw createError;
37021
- if (rollbackRetainsNewResource(current)) throw markNonRetryable(new CdkdError(maskSecretsInText(`Cannot reverse the replacement of ${op.logicalId} (${op.resourceType}): the re-create of the old resource (${prev.physicalId}) collided with the name still held by the new one (${current.physicalId}), and UpdateReplacePolicy: Retain pins that new resource in place, so cdkd will not delete it to free the name. Delete the new resource yourself, or remove UpdateReplacePolicy: Retain, then re-run \`cdkd rollback\` — the journal is kept, so the revert resumes from here. To leave THIS resource alone and let the rest of the rollback proceed, re-run with \`cdkd rollback --orphan ${op.logicalId}\`: one op failure stops the segment loop, so a single pinned resource otherwise halts every OLDER segment too. Underlying collision: ${msg}`, secrets), "NAMED_REPLACEMENT_COLLISION", maskSecretsInError(createError instanceof Error ? createError : void 0, secrets)));
37022
- logger.info(` Rollback: re-create collided with the new resource's name — deleting the new resource (${current.physicalId}) first...`);
37611
+ if (rollbackRetainsNewResource(current)) throw markNonRetryable(new CdkdError(maskSecretsInText(`Cannot reverse the replacement of ${safe(op.logicalId)} (${safe(op.resourceType)}): the re-create of the old resource (${safe(prev.physicalId)}) collided with the name still held by the new one (${safe(current.physicalId)}), and UpdateReplacePolicy: Retain pins that new resource in place, so cdkd will not delete it to free the name. Delete the new resource yourself, or remove UpdateReplacePolicy: Retain, then re-run \`cdkd rollback\` — the journal is kept, so the revert resumes from here. To leave THIS resource alone and let the rest of the rollback proceed, re-run with ` + (typeof op.logicalId === "string" && PASTEABLE_LOGICAL_ID.test(op.logicalId) ? `\`cdkd rollback --orphan ${op.logicalId}\`: one op failure stops the ` : "`cdkd rollback --orphan <id>` (the id is withheld: it is not a plain CloudFormation logical id, so a pasted command could be reshaped by the shell or name a different resource -- read it from `cdkd events`): one op failure stops the ") + `segment loop, so a single pinned resource otherwise halts every OLDER segment too. Underlying collision: ${displaySafe(msg)}`, secrets), "NAMED_REPLACEMENT_COLLISION", maskSecretsInError(createError instanceof Error ? createError : void 0, secrets)));
37612
+ logger.info(` Rollback: re-create collided with the new resource's name — deleting the new resource (${displaySafe(current.physicalId)}) first...`);
37023
37613
  {
37024
37614
  const finalSnapshotIdentifier = rollbackFinalSnapshotId(op.resourceType, current, op.provisionedBy);
37025
37615
  throwIfDeleteSkipped(await resolveNewDeleteProvider().delete(op.logicalId, current.physicalId, op.resourceType, current.properties, {
@@ -37036,12 +37626,12 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
37036
37626
  interruptedMessage: "Rollback interrupted while waiting for the old name to release"
37037
37627
  });
37038
37628
  } catch (recreateError) {
37039
- throw new Error(maskSecretsInText(`Failed to re-create the old ${op.logicalId} after the new resource (${current.physicalId}) was already deleted: ${recreateError instanceof Error ? recreateError.message : String(recreateError)}. The resource is now absent — fix forward with 'cdkd deploy'.`, secrets), { cause: maskSecretsInError(recreateError instanceof Error ? recreateError : void 0, secrets) });
37629
+ throw new Error(maskSecretsInText(`Failed to re-create the old ${safe(op.logicalId)} after the new resource (${current.physicalId}) was already deleted: ${displaySafe(recreateError instanceof Error ? recreateError.message : String(recreateError))}. The resource is now absent — fix forward with 'cdkd deploy'.`, secrets), { cause: maskSecretsInError(recreateError instanceof Error ? recreateError : void 0, secrets) });
37040
37630
  }
37041
37631
  }
37042
37632
  const adoptedLiveNewResource = !deletedNewFirst && createResult.physicalId === current.physicalId;
37043
37633
  if (adoptedLiveNewResource) {
37044
- logger.warn(` ⚠ ${op.logicalId} (${op.resourceType}): the re-create returned the LIVE new resource (${current.physicalId}) instead of re-creating the old one — its Create API is name-idempotent and the new resource still holds the same user-supplied name. Skipping the delete-new step (it would delete that very resource). The old resource's ORIGINAL properties may NOT have been re-applied; state now records the pre-replacement properties, so run 'cdkd drift ${stackName}' to inspect and 'cdkd deploy' to reconcile, or rename the resource to make the replacement reversible.`);
37634
+ logger.warn(` ⚠ ${safe(op.logicalId)} (${safe(op.resourceType)}): the re-create returned the LIVE new resource (${displaySafe(current.physicalId)}) instead of re-creating the old one — its Create API is name-idempotent and the new resource still holds the same user-supplied name. Skipping the delete-new step (it would delete that very resource). The old resource's ORIGINAL properties may NOT have been re-applied; state now records the pre-replacement properties, so run 'cdkd drift ${stackName}' to inspect and 'cdkd deploy' to reconcile, or rename the resource to make the replacement reversible.`);
37045
37635
  result.warnings++;
37046
37636
  }
37047
37637
  const { observedProperties: _staleObserved, ...prevRecord } = prev;
@@ -37065,11 +37655,11 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
37065
37655
  ...finalSnapshotIdentifier !== void 0 && { finalSnapshotIdentifier }
37066
37656
  }), op.logicalId, current.physicalId, "while deleting the new resource after re-creating the old one");
37067
37657
  } catch (deleteError) {
37068
- logger.warn(maskSecretsInText(` Rollback: old ${op.logicalId} re-created, but deleting the new resource (${current.physicalId}) failed: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}. Delete it manually — it is no longer tracked in state.`, secrets));
37658
+ logger.warn(maskSecretsInText(` Rollback: old ${safe(op.logicalId)} re-created, but deleting the new resource (${displaySafe(current.physicalId)}) failed: ${displaySafe(deleteError instanceof Error ? deleteError.message : String(deleteError))}. Delete it manually — it is no longer tracked in state.`, secrets));
37069
37659
  survivorReason = `The replacement's new ${op.resourceType} (${current.physicalId}) could not be deleted after the old resource was re-created: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}. It is live, still billing, and no longer tracked by cdkd — delete it yourself.`;
37070
37660
  result.warnings++;
37071
37661
  }
37072
- logger.info(adoptedLiveNewResource ? ` Rollback: ${op.logicalId} adopted the live resource (${createResult.physicalId}) — replacement NOT fully reversed (name-idempotent Create API)` : ` Rollback: ${op.logicalId} replacement reversed (old resource re-created as ${createResult.physicalId})`);
37662
+ logger.info(adoptedLiveNewResource ? ` Rollback: ${safe(op.logicalId)} adopted the live resource (${createResult.physicalId}) — replacement NOT fully reversed (name-idempotent Create API)` : ` Rollback: ${safe(op.logicalId)} replacement reversed (old resource re-created as ${createResult.physicalId})`);
37073
37663
  const survivorProvisionedBy = current.provisionedBy;
37074
37664
  ctx.recordEvent?.({
37075
37665
  eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
@@ -37088,18 +37678,18 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
37088
37678
  }
37089
37679
  case "revert": {
37090
37680
  if (!op.previousState) {
37091
- logger.warn(` Rollback: Cannot restore ${op.logicalId} — no previous state available`);
37681
+ logger.warn(` Rollback: Cannot restore ${safe(op.logicalId)} — no previous state available`);
37092
37682
  result.warnings++;
37093
37683
  return;
37094
37684
  }
37095
37685
  const previousState = op.previousState;
37096
37686
  const current = stateResources[op.logicalId];
37097
37687
  if (!current) {
37098
- logger.warn(` Rollback: Cannot restore ${op.logicalId} — resource not found in current state`);
37688
+ logger.warn(` Rollback: Cannot restore ${safe(op.logicalId)} — resource not found in current state`);
37099
37689
  result.warnings++;
37100
37690
  return;
37101
37691
  }
37102
- logger.info(` Rollback: Restoring ${op.logicalId} (${op.resourceType}) to previous state`);
37692
+ logger.info(` Rollback: Restoring ${safe(op.logicalId)} (${safe(op.resourceType)}) to previous state`);
37103
37693
  const { provider } = ctx.providerRegistry.getProviderFor({
37104
37694
  resourceType: op.resourceType,
37105
37695
  provisionedBy: op.provisionedBy
@@ -37121,8 +37711,8 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
37121
37711
  ], op.logicalId, logger, isInterrupted, secrets);
37122
37712
  stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(previousState, revertResult), secrets, previousState.properties);
37123
37713
  const rollbackPartial = updatePartialReason(revertResult);
37124
- if (rollbackPartial !== void 0) logger.warn(maskSecretsInText(` Rollback: ${op.logicalId} restored, ${updatePartialMessage(rollbackPartial)}`, secrets));
37125
- else logger.info(` Rollback: ${op.logicalId} restored successfully`);
37714
+ if (rollbackPartial !== void 0) logger.warn(maskSecretsInText(` Rollback: ${safe(op.logicalId)} restored, ${updatePartialMessage(rollbackPartial)}`, secrets));
37715
+ else logger.info(` Rollback: ${safe(op.logicalId)} restored successfully`);
37126
37716
  await afterOp?.(op.logicalId);
37127
37717
  ctx.recordEvent?.({
37128
37718
  eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
@@ -37137,7 +37727,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
37137
37727
  }
37138
37728
  }
37139
37729
  } catch (rollbackError) {
37140
- logger.warn(maskSecretsInText(` Rollback failed for ${op.logicalId} (${op.changeType}): ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, secrets));
37730
+ logger.warn(maskSecretsInText(` Rollback failed for ${safe(op.logicalId)} (${safe(op.changeType)}): ${displaySafe(rollbackError instanceof Error ? rollbackError.message : String(rollbackError))}`, secrets));
37141
37731
  logger.warn(" Continuing with remaining rollback operations...");
37142
37732
  result.failures++;
37143
37733
  const failedRoute = createRollbackRoute ?? op.provisionedBy;
@@ -37199,14 +37789,14 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
37199
37789
  try {
37200
37790
  switch (action) {
37201
37791
  case "skip-failed-noop":
37202
- logger.info(` Rollback: failed ${op.changeType} of ${op.logicalId} (${op.resourceType}) left nothing to revert, skipping`);
37792
+ logger.info(` Rollback: failed ${safe(op.changeType)} of ${safe(op.logicalId)} (${safe(op.resourceType)}) left nothing to revert, skipping`);
37203
37793
  break;
37204
37794
  case "skip-failed-unknown":
37205
- logger.warn(` Rollback: failed CREATE of ${op.logicalId} (${op.resourceType}) recorded no physical id — if it was partially created in AWS, delete it manually`);
37795
+ logger.warn(` Rollback: failed CREATE of ${safe(op.logicalId)} (${safe(op.resourceType)}) recorded no physical id — if it was partially created in AWS, delete it manually`);
37206
37796
  result.warnings++;
37207
37797
  break;
37208
37798
  case "skip-failed-absent":
37209
- logger.warn(` Rollback: cannot revert failed UPDATE of ${op.logicalId} — no previous state available, skipping`);
37799
+ logger.warn(` Rollback: cannot revert failed UPDATE of ${safe(op.logicalId)} — no previous state available, skipping`);
37210
37800
  result.warnings++;
37211
37801
  break;
37212
37802
  case "orphan-failed-create-retain": {
@@ -37223,7 +37813,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
37223
37813
  options.onOrphan?.(orphaned);
37224
37814
  }
37225
37815
  delete stateResources[op.logicalId];
37226
- logger.info(` Rollback: leaving partially-created ${op.logicalId} (${op.resourceType}) in AWS (DeletionPolicy: Retain) — removed from state`);
37816
+ logger.info(` Rollback: leaving partially-created ${safe(op.logicalId)} (${safe(op.resourceType)}) in AWS (DeletionPolicy: Retain) — removed from state`);
37227
37817
  await options.afterOp?.(op.logicalId);
37228
37818
  ctx.recordEvent?.({
37229
37819
  eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
@@ -37243,7 +37833,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
37243
37833
  const takeFinalSnapshot = snapshotPolicy && ctx.skipFinalSnapshot !== true;
37244
37834
  let finalSnapshotIdentifier;
37245
37835
  if (takeFinalSnapshot) finalSnapshotIdentifier = await prepareCreateRollbackFinalSnapshot(op, deleteProvisionedBy, ctx);
37246
- logger.info(` Rollback: deleting partially-created ${op.logicalId} (${op.resourceType}) (--revert-failed)` + (takeFinalSnapshot ? " — DeletionPolicy: Snapshot" : "") + (snapshotPolicy && !takeFinalSnapshot ? " — DeletionPolicy: Snapshot NOT taken (--skip-final-snapshot)" : ""));
37836
+ logger.info(` Rollback: deleting partially-created ${safe(op.logicalId)} (${safe(op.resourceType)}) (--revert-failed)` + (takeFinalSnapshot ? " — DeletionPolicy: Snapshot" : "") + (snapshotPolicy && !takeFinalSnapshot ? " — DeletionPolicy: Snapshot NOT taken (--skip-final-snapshot)" : ""));
37247
37837
  const { provider } = ctx.providerRegistry.getProviderFor({
37248
37838
  resourceType: op.resourceType,
37249
37839
  provisionedBy: deleteProvisionedBy
@@ -37267,7 +37857,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
37267
37857
  case "revert-failed-update": {
37268
37858
  const current = stateResources[op.logicalId];
37269
37859
  const prev = op.previousState;
37270
- logger.info(` Rollback: force-reverting failed UPDATE of ${op.logicalId} (${op.resourceType}) to its pre-deploy properties (--revert-failed; remote state is unknown)`);
37860
+ logger.info(` Rollback: force-reverting failed UPDATE of ${safe(op.logicalId)} (${safe(op.resourceType)}) to its pre-deploy properties (--revert-failed; remote state is unknown)`);
37271
37861
  const { provider } = ctx.providerRegistry.getProviderFor({
37272
37862
  resourceType: op.resourceType,
37273
37863
  provisionedBy: op.provisionedBy ?? current.provisionedBy
@@ -37289,8 +37879,8 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
37289
37879
  ], op.logicalId, logger, options.isInterrupted, secrets);
37290
37880
  stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(prev, revertFailedResult), secrets, prev.properties);
37291
37881
  const revertFailedPartial = updatePartialReason(revertFailedResult);
37292
- if (revertFailedPartial !== void 0) logger.warn(maskSecretsInText(` Rollback: ${op.logicalId} reverted, ${updatePartialMessage(revertFailedPartial)}`, secrets));
37293
- else logger.info(` Rollback: ${op.logicalId} reverted successfully`);
37882
+ if (revertFailedPartial !== void 0) logger.warn(maskSecretsInText(` Rollback: ${safe(op.logicalId)} reverted, ${updatePartialMessage(revertFailedPartial)}`, secrets));
37883
+ else logger.info(` Rollback: ${safe(op.logicalId)} reverted successfully`);
37294
37884
  await options.afterOp?.(op.logicalId);
37295
37885
  ctx.recordEvent?.({
37296
37886
  eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
@@ -37305,7 +37895,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
37305
37895
  }
37306
37896
  }
37307
37897
  } catch (revertError) {
37308
- logger.warn(maskSecretsInText(` Rollback failed for failed-op ${op.logicalId} (${op.changeType}): ${revertError instanceof Error ? revertError.message : String(revertError)}`, secrets));
37898
+ logger.warn(maskSecretsInText(` Rollback failed for failed-op ${safe(op.logicalId)} (${safe(op.changeType)}): ${displaySafe(revertError instanceof Error ? revertError.message : String(revertError))}`, secrets));
37309
37899
  result.failures++;
37310
37900
  pending.add(op);
37311
37901
  const failedRoute = createRollbackRoute ?? op.provisionedBy;
@@ -41041,5 +41631,5 @@ var DeployEngine = class DeployEngine {
41041
41631
  };
41042
41632
 
41043
41633
  //#endregion
41044
- export { getCurrentResourceSecrets as $, validateAssetBucketName as $n, DependencyError as $r, findActionableSilentDrops as $t, planOrphanAdoption as A, CUSTOM_RESOURCE_RESPONSE_PREFIX as An, warnDeprecatedNoPrefixCliFlag as Ar, resolveExplicitPhysicalId as At, bold as B, WorkGraph as Bn, derivePartitionAndUrlSuffix as Br, requireConfigObject as Bt, createPreDeleteFinalSnapshot as C, markNonRetryable as Ci, S3StateBackend as Cn, resolveAutoAssetStorage as Cr, coerceParameterTypedValue as Ct, makeCanonicalizePropertiesFn as D, forceQuitRecoveryClause as Dn, resolveStateBucketWithDefaultAndSource as Dr, refStateLookupFromResource as Dt, unsupportedFinalSnapshotError as E, __exportAll as Ei, buildLockContentionMessage as En, resolveStateBucketWithDefault as Er, parameterTypeMayLoseSecretIdentity as Et, WARM_THROUGHPUT_MEMBERS as F, orphansAfterRollback as Fn, uploadCfnTemplate as Fr, configIntegerRefusal as Ft, yellow as G, AssetModeResolver as Gn, AwsClients as Gr, s3BucketArn as Gt, gray as H, createAssetRedirectResolver as Hn, processStackMessages as Hr, classifyReplaySecretRegion as Ht, coerceWarmThroughput as I, orphansCarriedFrom as In, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as Ir, configStringRefusal as It, exportAliasCollisionScrubWarning as J, ensureAssetStorage as Jn, setAwsClients as Jr, s3BucketRegionalDomainName as Jt, collectDeclaredOutputNames as K, BOOTSTRAP_MARKER_PREFIX as Kn, getAwsClients as Kr, s3BucketDomainName as Kt, isWarmThroughputDecrease as L, shouldRetainResource as Ln, expectedOwnerParam as Lr, readConfigString as Lt, isStatefulRecreateTargetForReplace as M, exportNamesCarriedFrom as Mn, CFN_TEMPLATE_URL_LIMIT as Mr, coerceCfnBoolean as Mt, isStatefulRecreateTargetSync as N, importableOutputKeys as Nn, MIGRATE_TMP_PREFIX as Nr, coerceCfnInteger as Nt, extractDeploymentEventError as O, formatLockExpiry as On, resolveUseCdkBootstrapAssets as Or, WAFv2WebACLProvider as Ot, renderStatefulReason as P, importableOutputs as Pn, findLargeInlineResources as Pr, configBooleanRefusal as Pt, secretSafeKeyDisplay as Q, readBootstrapMarkerBody as Qn, CrossAccountSecretRefusalError as Qr, INTRINSIC_KEYS as Qt, toFiniteNumber as R, AssetPublisher as Rn, PARTITION_TABLE as Rr, replayWarn as Rt, ccRoutedFinalSnapshotError as S, isTransientServerError as Si, LockManager as Sn, resolveApp as Sr, cfnRefValueFromPhysicalId as St, refusesFinalSnapshot as T, retryClassificationText as Ti, buildForceUnlockCommand as Tn, resolveSkipPrefix as Tr, isUnboundTemplateParameter as Tt, green as U, loadPublishableAssetManifest as Un, clearBucketRegionCache as Ur, producerRegionsFromState as Ut, cyan as V, buildAssetRedirectMap as Vn, AssemblyReader as Vr, requireConfigString as Vt, red as W, rewriteTemplateAssetReferences as Wn, resolveBucketRegion as Wr, withSharedDrainBudget as Wt, secretBearing as X, isCrossRegionRedirect as Xn, CdkdError as Xr, applyRoleArnIfSet as Xt, isExportAliasCollision as Y, getBootstrapMarkerKey as Yn, AssetError as Yr, s3BucketWebsiteUrl as Yt, secretBearingStateKeyWarning as Z, parseBootstrapMarker as Zn, ConfigError as Zr, DiffCalculator as Zt, isSecretBearingReferenceString as _, normalizeAwsError as _i, maskSecretsInText as _n, getDockerImageBySourceHash as _r, deleteSkipReason as _t, DeploymentEventsStore as a, LockError as ai, STATE_SOURCED_BASELINE_RULES as an, describeDockerExecFailure as ar, createMaskedRetryLogger as at, PRE_DELETE_SNAPSHOT_TYPES as b, isRetryableTransientError as bi, redactSecretsForState as bn, getDefaultStateBucketName as br, IntrinsicFunctionResolver as bt, replayFailedOperations as c, ProvisioningError as ci, TEMPLATE_SOURCED_RULES as cn, formatDockerLoginError as cr, beginCommandInterruptScope as ct, updatePartialReason as d, StackHasActiveImportsError as di, dynamicReferenceTokens as dn, redactDockerArgvValues as dr, isInterruptedWaitError as dt, DeployCancelledError as ei, findSilentDropProperties as en, validateContainerRepoName as er, IAMRoleProvider as et, withResourceDeadline as f, StackTerminationProtectionError as fi, errorCauseChain as fn, runDockerForeground as fr, startInterruptWatch as ft, bagHoldsSecretExpression as g, isCdkdError as gi, maskSecretsInError as gn, AssetManifestLoader as gr, deleteIndeterminateGuards as gt, computeImplicitDeleteEdges as h, formatError as hi, markSameGenerationBag as hn, stripControlChars as hr, UNSPECIFIED_SKIP_REASON as ht, DeploymentEventsReader as i, LocalStartServiceError as ii, TemplateParser as in, describeDockerCapturedOutput as ir, wouldReturnToSdkProvider as it, MULTI_REGION_RECREATE_BLOCKED_TYPES as j, DEFAULT_STATE_PREFIX as jn, CFN_TEMPLATE_BODY_LIMIT as jr, assertRegionMatch as jt, makeSiblingClaimReader as k, shellQuote as kn, stateBucketExistenceConfirmed as kr, normalizeAwsTagsToCfn as kt, replayRollback as l, ResourceTimeoutError as li, carriesSecretMask as ln, getDockerCmd as lr, endCommandInterruptScope as lt, IMPLICIT_DELETE_DEPENDENCIES as m, SynthesisError as mi, isSingleDynamicReferenceToken as mn, escapeRegExp$1 as mr, slowCcOperationTimeoutMs as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, IntrinsicResolutionRefusalError as ni, withRetry as nn, describeAwsFailure as nr, clearOnUpdateRemoval as nt, planFailedOps as o, NestedStackChildDirectDestroyError as oi, STATE_SOURCED_CROSS_GENERATION_RULES as on, describeDockerFailure as or, maskDeep as ot, maskingRetryLogger as p, StateError as pi, identityKeyFor as pn, runDockerStreaming as pr, CloudControlProvider as pt, collectPublishedOutputNames as q, assertAssetBucketRegion as qn, resetAwsClients as qr, s3BucketDualStackDomainName as qt, DeployEngine as r, LocalInvokeBuildError as ri, DagBuilder as rn, buildDockerImage as rr, ProviderRegistry as rt, planRollback as s, PartialFailureError as si, STATE_SOURCED_READBACK_RULES as sn, dockerSpawnEnvWithSensitive as sr, maskerOrIdentity as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, DynamicReferenceRegionAmbiguousError as ti, describeTypeWithThrottleRetry as tn, buildDenyExternalAccessPolicy as tr, collectInlinePolicyNamesManagedBySiblings as tt, updatePartialMessage as u, ResourceUpdateNotSupportedError as ui, createSecretMasker as un, partitionSensitiveEnv as ur, interruptWatchListenerCount as ut, bindingSkippedOutputs as v, withErrorHandling as vi, recordMaskOnlyValue as vn, Synthesizer as vr, disableInstanceApiTermination as vt, isFinalSnapshotError as w, markRedactedCause as wi, rebuildClientForBucketRegion as wn, resolveCaptureObservedState as wr, getAccountInfo as wt, buildFinalSnapshotIdentifier as x, isThrottlingError as xi, scrubResourceRecord as xn, getLegacyStateBucketName as xr, carriesDynamicReference as xt, ATOMIC_FINAL_SNAPSHOT_TYPES as y, isMarkedNonRetryable as yi, recoverMaskedOutput as yn, synthesisStatusMessage as yr, isTerminationProtectionPropagationError as yt, formatResourceLine as z, stringifyValue as zn, canonicalizeRegion as zr, requireConfigArray as zt };
41045
- //# sourceMappingURL=deploy-engine-Nn9j2GPK.js.map
41634
+ export { secretSafeKeyDisplay as $, parseBootstrapMarker as $n, ConfigError as $r, DiffCalculator as $t, planOrphanAdoption as A, formatLockExpiry as An, resolveUseCdkBootstrapAssets as Ar, WAFv2WebACLProvider as At, formatResourceLine as B, AssetPublisher as Bn, PARTITION_TABLE as Br, replayWarn as Bt, createPreDeleteFinalSnapshot as C, isThrottlingError as Ci, scrubResourceRecord as Cn, getLegacyStateBucketName as Cr, cfnRefValueFromPhysicalId as Ct, makeCanonicalizePropertiesFn as D, retryClassificationText as Di, buildForceUnlockCommand as Dn, resolveSkipPrefix as Dr, parameterTypeMayLoseSecretIdentity as Dt, unsupportedFinalSnapshotError as E, markRedactedCause as Ei, rebuildClientForBucketRegion as En, resolveCaptureObservedState as Er, isUnboundTemplateParameter as Et, WARM_THROUGHPUT_MEMBERS as F, importableOutputKeys as Fn, MIGRATE_TMP_PREFIX as Fr, coerceCfnInteger as Ft, red as G, loadPublishableAssetManifest as Gn, clearBucketRegionCache as Gr, producerRegionsFromState as Gt, cyan as H, WorkGraph as Hn, derivePartitionAndUrlSuffix as Hr, requireConfigObject as Ht, coerceWarmThroughput as I, importableOutputs as In, findLargeInlineResources as Ir, configBooleanRefusal as It, collectPublishedOutputNames as J, BOOTSTRAP_MARKER_PREFIX as Jn, getAwsClients as Jr, s3BucketDomainName as Jt, yellow as K, rewriteTemplateAssetReferences as Kn, resolveBucketRegion as Kr, withSharedDrainBudget as Kt, isWarmThroughputDecrease as L, orphansAfterRollback as Ln, uploadCfnTemplate as Lr, configIntegerRefusal as Lt, isStatefulRecreateTargetForReplace as M, CUSTOM_RESOURCE_RESPONSE_PREFIX as Mn, warnDeprecatedNoPrefixCliFlag as Mr, resolveExplicitPhysicalId as Mt, isStatefulRecreateTargetSync as N, DEFAULT_STATE_PREFIX as Nn, CFN_TEMPLATE_BODY_LIMIT as Nr, assertRegionMatch as Nt, extractDeploymentEventError as O, __exportAll as Oi, buildLockContentionMessage as On, resolveStateBucketWithDefault as Or, refStateLookupFromResource as Ot, renderStatefulReason as P, exportNamesCarriedFrom as Pn, CFN_TEMPLATE_URL_LIMIT as Pr, coerceCfnBoolean as Pt, secretBearingStateKeyWarning as Q, isCrossRegionRedirect as Qn, CdkdError as Qr, applyRoleArnIfSet as Qt, toCfnInteger as R, orphansCarriedFrom as Rn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as Rr, configStringRefusal as Rt, ccRoutedFinalSnapshotError as S, isRetryableTransientError as Si, redactSecretsForState as Sn, getDefaultStateBucketName as Sr, carriesDynamicReference as St, refusesFinalSnapshot as T, markNonRetryable as Ti, S3StateBackend as Tn, resolveAutoAssetStorage as Tr, getAccountInfo as Tt, gray as U, buildAssetRedirectMap as Un, AssemblyReader as Ur, requireConfigString as Ut, bold as V, stringifyValue as Vn, canonicalizeRegion as Vr, requireConfigArray as Vt, green as W, createAssetRedirectResolver as Wn, processStackMessages as Wr, classifyReplaySecretRegion as Wt, isExportAliasCollision as X, ensureAssetStorage as Xn, setAwsClients as Xr, s3BucketRegionalDomainName as Xt, exportAliasCollisionScrubWarning as Y, assertAssetBucketRegion as Yn, resetAwsClients as Yr, s3BucketDualStackDomainName as Yt, secretBearing as Z, getBootstrapMarkerKey as Zn, AssetError as Zr, s3BucketWebsiteUrl as Zt, isSecretBearingReferenceString as _, formatError as _i, markSameGenerationBag as _n, stripControlChars as _r, deleteIndeterminateGuards as _t, DeploymentEventsStore as a, LocalInvokeBuildError as ai, DagBuilder as an, buildDockerImage as ar, wouldReturnToSdkProvider as at, PRE_DELETE_SNAPSHOT_TYPES as b, withErrorHandling as bi, recordMaskOnlyValue as bn, Synthesizer as br, isTerminationProtectionPropagationError as bt, replayFailedOperations as c, NestedStackChildDirectDestroyError as ci, STATE_SOURCED_CROSS_GENERATION_RULES as cn, describeDockerFailure as cr, maskerOrIdentity as ct, updatePartialReason as d, ResourceTimeoutError as di, carriesSecretMask as dn, getDockerCmd as dr, interruptWatchListenerCount as dt, CrossAccountSecretRefusalError as ei, INTRINSIC_KEYS as en, readBootstrapMarkerBody as er, getCurrentResourceSecrets as et, withResourceDeadline as f, ResourceUpdateNotSupportedError as fi, createSecretMasker as fn, partitionSensitiveEnv as fr, isInterruptedWaitError as ft, bagHoldsSecretExpression as g, SynthesisError as gi, isSingleDynamicReferenceToken as gn, escapeRegExp$1 as gr, UNSPECIFIED_SKIP_REASON as gt, computeImplicitDeleteEdges as h, StateError as hi, identityKeyFor as hn, runDockerStreaming as hr, slowCcOperationTimeoutMs as ht, DeploymentEventsReader as i, IntrinsicResolutionRefusalError as ii, withRetry as in, describeAwsFailure as ir, ProviderRegistry as it, MULTI_REGION_RECREATE_BLOCKED_TYPES as j, shellQuote as jn, stateBucketExistenceConfirmed as jr, normalizeAwsTagsToCfn as jt, makeSiblingClaimReader as k, forceQuitRecoveryClause as kn, resolveStateBucketWithDefaultAndSource as kr, isSettledInstanceState as kt, replayRollback as l, PartialFailureError as li, STATE_SOURCED_READBACK_RULES as ln, dockerSpawnEnvWithSensitive as lr, beginCommandInterruptScope as lt, IMPLICIT_DELETE_DEPENDENCIES as m, StackTerminationProtectionError as mi, errorCauseChain as mn, runDockerForeground as mr, CloudControlProvider as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, DeployCancelledError as ni, findSilentDropProperties as nn, validateContainerRepoName as nr, collectInlinePolicyNamesManagedBySiblings as nt, planFailedOps as o, LocalStartServiceError as oi, TemplateParser as on, describeDockerCapturedOutput as or, createMaskedRetryLogger as ot, maskingRetryLogger as p, StackHasActiveImportsError as pi, dynamicReferenceTokens as pn, redactDockerArgvValues as pr, startInterruptWatch as pt, collectDeclaredOutputNames as q, AssetModeResolver as qn, AwsClients as qr, s3BucketArn as qt, DeployEngine as r, DynamicReferenceRegionAmbiguousError as ri, describeTypeWithThrottleRetry as rn, buildDenyExternalAccessPolicy as rr, clearOnUpdateRemoval as rt, planRollback as s, LockError as si, STATE_SOURCED_BASELINE_RULES as sn, describeDockerExecFailure as sr, maskDeep as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, DependencyError as ti, findActionableSilentDrops as tn, validateAssetBucketName as tr, IAMRoleProvider as tt, updatePartialMessage as u, ProvisioningError as ui, TEMPLATE_SOURCED_RULES as un, formatDockerLoginError as ur, endCommandInterruptScope as ut, bindingSkippedOutputs as v, isCdkdError as vi, maskSecretsInError as vn, AssetManifestLoader as vr, deleteSkipReason as vt, isFinalSnapshotError as w, isTransientServerError as wi, LockManager as wn, resolveApp as wr, coerceParameterTypedValue as wt, buildFinalSnapshotIdentifier as x, isMarkedNonRetryable as xi, recoverMaskedOutput as xn, synthesisStatusMessage as xr, IntrinsicFunctionResolver as xt, ATOMIC_FINAL_SNAPSHOT_TYPES as y, normalizeAwsError as yi, maskSecretsInText as yn, getDockerImageBySourceHash as yr, disableInstanceApiTermination as yt, toFiniteNumber as z, shouldRetainResource as zn, expectedOwnerParam as zr, readConfigString as zt };
41635
+ //# sourceMappingURL=deploy-engine-0Wg4MwP2.js.map