@go-to-k/cdkd 0.290.32 → 0.290.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { C as displayAwsMessage, D as truncateCodePoints, E as isPasteableIdent, S as UNRENDERABLE, T 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, w as displayIdent, x as STACK_REF_MAX_CODE_POINTS, y as withoutGeneratedFallbackName } from "./logger-BX4pJk0T.js";
2
2
  import { a as setAssumedRoleCredentials, i as getPreAssumeEnvCredentials, n as awsClientDefaults, o as setPreAssumeEnvCredentials, r as getAssumedRoleCredentials, t as SECOND_ROLE_PUBLISH_REFUSAL } from "./aws-client-defaults-BoIxyCir.js";
3
- import { t as getCdkdVersion } from "./version-F41ektvu.js";
3
+ import { t as getCdkdVersion } from "./version-CpyoQ3PO.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";
@@ -9802,6 +9802,7 @@ function refuseMalformedOperation(shownStack, where, op) {
9802
9802
  const attempted = o["attemptedProperties"];
9803
9803
  if (attempted !== void 0 && (typeof attempted !== "object" || attempted === null || Array.isArray(attempted))) fail("attemptedProperties", attempted, "an object when present");
9804
9804
  if (o["oldResourceRetained"] !== void 0 && typeof o["oldResourceRetained"] !== "boolean") fail("oldResourceRetained", o["oldResourceRetained"], "a boolean when present");
9805
+ if (o["previousResourceType"] !== void 0 && typeof o["previousResourceType"] !== "string") fail("previousResourceType", o["previousResourceType"], "a string when present");
9805
9806
  }
9806
9807
  /** The TYPE of a JSON-derived value, for a refusal that must not echo the value. */
9807
9808
  function kind(v) {
@@ -13071,7 +13072,7 @@ function storeAssociation(associations, key, expression, plaintext) {
13071
13072
  * matters: an AWS resource type string is fixed by AWS, and a typo in either
13072
13073
  * copy makes that copy's gate simply never fire (no-op), never fire wrongly.
13073
13074
  */
13074
- const NESTED_STACK_RESOURCE_TYPE$2 = "AWS::CloudFormation::Stack";
13075
+ const NESTED_STACK_RESOURCE_TYPE$4 = "AWS::CloudFormation::Stack";
13075
13076
  /**
13076
13077
  * What a PARENT stack's resolution proved about each `Parameters` entry of an
13077
13078
  * `AWS::CloudFormation::Stack` row it is about to provision, keyed by the
@@ -13410,7 +13411,7 @@ const UNFRAMED_SPELLING = Symbol("cdkd.nested-parameter.unframed-spelling");
13410
13411
  * it either -- it walks the child's stored bag with no inherited bag.
13411
13412
  */
13412
13413
  function recordNestedStackParameterExpressions(secrets, resourceType, resolvedProperties, sourceProperties, rules = TEMPLATE_DERIVED_RULES) {
13413
- if (resourceType !== NESTED_STACK_RESOURCE_TYPE$2) return;
13414
+ if (resourceType !== NESTED_STACK_RESOURCE_TYPE$4) return;
13414
13415
  if (secrets.size === 0) return;
13415
13416
  if (!isPlainObject$2(resolvedProperties) || !isPlainObject$2(sourceProperties)) return;
13416
13417
  if (!Object.hasOwn(resolvedProperties, "Parameters")) return;
@@ -18705,6 +18706,7 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
18705
18706
  silentDrop: /* @__PURE__ */ new Map([
18706
18707
  ["CapacityProviderConfiguration", "not yet implemented by cdkd"],
18707
18708
  ["FilesystemConfigurations", "not yet implemented by cdkd"],
18709
+ ["PlatformVersion", "not yet implemented by cdkd"],
18708
18710
  ["RequestHeaderConfiguration", "not yet implemented by cdkd"],
18709
18711
  ["Tags", "not yet implemented by cdkd"]
18710
18712
  ]),
@@ -24103,6 +24105,88 @@ function classifyReplaySecretRegion(expression, consumerRegion, importedProducer
24103
24105
  };
24104
24106
  }
24105
24107
 
24108
+ //#endregion
24109
+ //#region src/deployment/stale-attribute-heal.ts
24110
+ /**
24111
+ * Control-flow signal from the physical-id fallback to `resolveGetAtt`'s heal
24112
+ * wrapper. It never escapes that wrapper: it is raised only under a `probe`
24113
+ * context, which only the wrapper builds, and the wrapper catches it on the
24114
+ * same `await`. An `Error` subclass so a lint rule or a stray `catch` that
24115
+ * assumes `Error` still behaves.
24116
+ */
24117
+ var StaleAttributeMissSignal = class StaleAttributeMissSignal extends Error {
24118
+ constructor() {
24119
+ super("stale attribute miss (internal control flow — must not escape resolveGetAtt)");
24120
+ this.name = "StaleAttributeMissSignal";
24121
+ Object.setPrototypeOf(this, StaleAttributeMissSignal.prototype);
24122
+ }
24123
+ };
24124
+ const NESTED_STACK_RESOURCE_TYPE$3 = "AWS::CloudFormation::Stack";
24125
+ /**
24126
+ * Types whose `attributes` are NOT an AWS read-back: a custom resource's are its
24127
+ * handler's response `Data` (possibly `NoEcho`, and re-invoking the handler is
24128
+ * not a read), a nested stack's are its child's outputs.
24129
+ */
24130
+ function isHealExcludedType(resourceType) {
24131
+ return resourceType === NESTED_STACK_RESOURCE_TYPE$3 || resourceType === "AWS::CloudFormation::CustomResource" || resourceType.startsWith("Custom::");
24132
+ }
24133
+ /**
24134
+ * Drop every member that is not a value: `undefined`, `null` and `''`.
24135
+ *
24136
+ * Empty-to-absent is the point for the `--no-wait` `DBInstance` row
24137
+ * (go-to-k/cdkd#3077): an instance still `creating` has no endpoint, and an
24138
+ * empty value persisted into the record would be SERVED by the resolver's
24139
+ * cached read forever, shadowing every later heal.
24140
+ */
24141
+ function normalizeHealedAttributes(attributes) {
24142
+ const out = {};
24143
+ if (attributes === void 0 || attributes === null || typeof attributes !== "object") return out;
24144
+ for (const [key, value] of Object.entries(attributes)) {
24145
+ if (value === void 0 || value === null || value === "") continue;
24146
+ if (key === "__proto__") continue;
24147
+ out[key] = value;
24148
+ }
24149
+ return out;
24150
+ }
24151
+ /**
24152
+ * Read `attributeName` out of a healed map the way `resolveGetAtt` reads the
24153
+ * recorded one: flat key first (SDK providers record `Endpoint.Port` flat), then
24154
+ * a dot-path walk (Cloud Control records nested objects). `Object.hasOwn` at
24155
+ * every step — `attributeName` is template text (issue #2767).
24156
+ */
24157
+ function readHealedAttribute(attributes, attributeName) {
24158
+ if (Object.hasOwn(attributes, attributeName)) {
24159
+ const flat = attributes[attributeName];
24160
+ if (flat !== void 0 && flat !== null && flat !== "") return flat;
24161
+ }
24162
+ if (!attributeName.includes(".")) return void 0;
24163
+ let cursor = attributes;
24164
+ for (const part of attributeName.split(".")) if (cursor !== null && typeof cursor === "object" && Object.hasOwn(cursor, part)) cursor = cursor[part];
24165
+ else return;
24166
+ return cursor === null || cursor === "" ? void 0 : cursor;
24167
+ }
24168
+ /**
24169
+ * Merge a healed map INTO a record's attributes: a healed key is added only
24170
+ * where the record holds NO value for it (or holds one `isStaleValue` declares
24171
+ * unusable — the pre-#1681 placeholder ARN). A recorded value always wins
24172
+ * otherwise: create-time attributes the read does not report must survive, and
24173
+ * a read must never rewrite what a provider recorded.
24174
+ *
24175
+ * Returns the SAME `recorded` reference when nothing was added, so a caller can
24176
+ * tell "healed" from "nothing to do" by identity.
24177
+ */
24178
+ function mergeHealedAttributes(recorded, healed, isStaleValue) {
24179
+ let merged;
24180
+ for (const [key, value] of Object.entries(healed)) {
24181
+ if (value === void 0 || value === null || value === "") continue;
24182
+ if (key === "__proto__") continue;
24183
+ if (recorded !== void 0 && Object.hasOwn(recorded, key) && !isStaleValue(key, recorded[key])) continue;
24184
+ merged ??= { ...recorded };
24185
+ merged[key] = value;
24186
+ }
24187
+ return merged ?? recorded;
24188
+ }
24189
+
24106
24190
  //#endregion
24107
24191
  //#region src/provisioning/config-shape.ts
24108
24192
  /**
@@ -25757,6 +25841,20 @@ const REF_RETURNS_ARN_FROM_STATE = /* @__PURE__ */ new Map([
25757
25841
  ["AWS::AppSync::Resolver", ["ResolverArn"]]
25758
25842
  ]);
25759
25843
  /**
25844
+ * Is `value`, recorded under `attributeName` of a `resourceType` record, a
25845
+ * pre-#1681 PLACEHOLDER ARN — a value that is present but knowably unusable?
25846
+ *
25847
+ * The one predicate behind `rejectPlaceholderArnAttribute`'s refusal and the
25848
+ * #1852 heal's "this recorded value may be overwritten" exception
25849
+ * (`mergeHealedAttributes`), so the two cannot disagree about which records are
25850
+ * stale. Scoped to the {@link REF_RETURNS_ARN_FROM_STATE} types AND their
25851
+ * declared ARN attribute names — see that refusal's note for why no wider.
25852
+ */
25853
+ function isStalePlaceholderArnAttribute(resourceType, attributeName, value) {
25854
+ if (!REF_RETURNS_ARN_FROM_STATE.get(resourceType)?.includes(attributeName)) return false;
25855
+ return typeof value === "string" && isPlaceholderArn(value);
25856
+ }
25857
+ /**
25760
25858
  * Build a {@link RefStateLookup} from a resource's stored state maps, checking
25761
25859
  * `properties` first (the template value CFn `Ref` mirrors) then `attributes`.
25762
25860
  * Only non-empty string values qualify — an intrinsic-shaped or empty value is
@@ -26074,7 +26172,7 @@ function carriesDynamicReference(value) {
26074
26172
  return false;
26075
26173
  }
26076
26174
  /** The nested-stack resource type, whose `Outputs.<Name>` attributes are re-resolved (issue #2055). */
26077
- const NESTED_STACK_RESOURCE_TYPE$1 = "AWS::CloudFormation::Stack";
26175
+ const NESTED_STACK_RESOURCE_TYPE$2 = "AWS::CloudFormation::Stack";
26078
26176
  /** Prefix `NestedStackProvider` records a child stack output under. */
26079
26177
  const NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX = "Outputs.";
26080
26178
  /**
@@ -28070,14 +28168,19 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
28070
28168
  const storedValue = Object.hasOwn(resource.attributes, attributeName) ? resource.attributes[attributeName] : void 0;
28071
28169
  const flatValue = isImpossibleEmptyStoredAttribute(resource.resourceType, attributeName, storedValue) ? void 0 : storedValue;
28072
28170
  if (flatValue !== void 0) {
28073
- this.rejectPlaceholderArnAttribute(resource, attributeName, flatValue, logicalId, context);
28171
+ if (isStalePlaceholderArnAttribute(resource.resourceType, attributeName, flatValue)) {
28172
+ const outcome = await this.healStaleAttributes(logicalId, resource, context);
28173
+ const healed = this.usableHealedAttribute(outcome, attributeName);
28174
+ if (healed !== void 0 && !isStalePlaceholderArnAttribute(resource.resourceType, attributeName, healed)) return this.serveHealedAttribute(logicalId, attributeName, healed, context);
28175
+ this.rejectPlaceholderArnAttribute(resource, attributeName, flatValue, logicalId, context, outcome);
28176
+ }
28074
28177
  if (resource.resourceType === "AWS::Route53::HostedZone" && attributeName === "NameServers" && typeof flatValue === "string") {
28075
28178
  const nameServers = flatValue === "" ? [] : flatValue.split(",");
28076
28179
  this.logger.debug(`Normalized legacy Fn::GetAtt attribute: ${this.displayMasked(logicalId, context)}.${this.displayMasked(attributeName, context)} -> ${this.displayMasked(stringifyAttributeForLog(attributeName, this.maskValueLeaves(nameServers, context)), context)}`);
28077
28180
  return this.noteAttributeSecrecy(logicalId, attributeName, nameServers, context);
28078
28181
  }
28079
28182
  this.logger.debug(`Resolved Fn::GetAtt from attributes: ${this.displayMasked(logicalId, context)}.${this.displayMasked(attributeName, context)} -> ${this.displayMasked(stringifyAttributeForLog(attributeName, this.maskValueLeaves(flatValue, context)), context)}`);
28080
- if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE$1 && attributeName.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX) && carriesDynamicReference(flatValue)) return await this.reresolveCrossStackValue(flatValue, nestedStackChildRegionFromLocalArn(resource.physicalId), context, `nested stack ${logicalId} ${this.displayLeaf(attributeName, context)}`, crossStackSourceKey({ "Fn::GetAtt": getAtt }));
28183
+ if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE$2 && attributeName.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX) && carriesDynamicReference(flatValue)) return await this.reresolveCrossStackValue(flatValue, nestedStackChildRegionFromLocalArn(resource.physicalId), context, `nested stack ${logicalId} ${this.displayLeaf(attributeName, context)}`, crossStackSourceKey({ "Fn::GetAtt": getAtt }));
28081
28184
  return this.noteAttributeSecrecy(logicalId, attributeName, flatValue, context);
28082
28185
  }
28083
28186
  if (attributeName.includes(".")) {
@@ -28094,13 +28197,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
28094
28197
  }
28095
28198
  }
28096
28199
  }
28097
- if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE$1 && attributeName.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX)) {
28200
+ if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE$2 && attributeName.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX)) {
28098
28201
  const declared = Object.keys(resource.attributes ?? {}).filter((k) => k.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX)).sort().map((k) => this.displayMasked(this.outputNameLogText(k, context), context));
28099
28202
  const declaredText = declared.length > 0 ? declared.join(", ") : "(none)";
28100
28203
  const loggedLogicalId = this.displayMasked(logicalId, context);
28101
28204
  throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${loggedLogicalId}, ${this.displayMasked(attributeName, context)}]: the nested stack '${loggedLogicalId}' declares no output named '${this.displayMasked(this.outputNameLogText(attributeName, context), context)}'. Its outputs are ${this.displayMasked(declaredText, context)}. Check the output name in the nested stack's template, and deploy the child stack again if you have just added it.`));
28102
28205
  }
28103
- const value = await this.constructGuardedAttribute(resource, attributeName, context, logicalId);
28206
+ const value = await this.constructWithStaleRecordHeal(resource, attributeName, context, logicalId);
28104
28207
  this.logger.debug(`Resolved Fn::GetAtt: ${this.displayMasked(logicalId, context)}.${this.displayMasked(attributeName, context)} -> ${this.displayMasked(stringifyAttributeForLog(attributeName, this.maskValueLeaves(value, context)), context)}`);
28105
28208
  return value;
28106
28209
  }
@@ -28191,10 +28294,143 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
28191
28294
  * honest answer is to say so and name the remedy: the record heals on the
28192
28295
  * resource's next in-place update (#1727).
28193
28296
  */
28194
- rejectPlaceholderArnAttribute(resource, attributeName, value, logicalId, context) {
28195
- if (!REF_RETURNS_ARN_FROM_STATE.get(resource.resourceType)?.includes(attributeName)) return;
28196
- if (typeof value !== "string" || !isPlaceholderArn(value)) return;
28197
- throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(attributeName, context)}] for ${resource.resourceType}: the recorded value "${stringifyValue(this.maskValueLeaves(value, context))}" is a placeholder written by a cdkd version older than issue #1681 — its region and account fields are literal wildcards, so it is not a usable ARN. Deploy the stack again so the resource's next update heals the record (cdkd now records the real ARN), or re-import the resource.`));
28297
+ rejectPlaceholderArnAttribute(resource, attributeName, value, logicalId, context, healOutcome) {
28298
+ if (!isStalePlaceholderArnAttribute(resource.resourceType, attributeName, value)) return;
28299
+ throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(attributeName, context)}] for ${resource.resourceType}: the recorded value "${stringifyValue(this.maskValueLeaves(value, context))}" is a placeholder written by a cdkd version older than issue #1681 — its region and account fields are literal wildcards, so it is not a usable ARN. ` + this.staleRecordRemedy(healOutcome, context)));
28300
+ }
28301
+ /**
28302
+ * The remedy half of a STALE-RECORD refusal (issue
28303
+ * [#1852](https://github.com/go-to-k/cdkd/issues/1852)), worded from what the
28304
+ * heal observed so it is true on the path taken.
28305
+ *
28306
+ * The sentence it replaces — "deploy the stack again so the resource's next
28307
+ * update heals the record" — was false for the commonest case: a deploy that
28308
+ * changes none of the resource's own properties takes the no-change skip and
28309
+ * never runs `update()`, so "again" healed nothing.
28310
+ *
28311
+ * Every clause is cdkd-authored; AWS's text stays behind `--verbose`
28312
+ * ({@link describeFailureObserved}), because a denied read quotes the
28313
+ * caller's account, role and session.
28314
+ */
28315
+ staleRecordRemedy(outcome, context) {
28316
+ const touch = "change any property of the resource so its next update re-records the attributes, or re-import it with 'cdkd import'";
28317
+ if (outcome === void 0) return `Run 'cdkd deploy': it re-reads a stale record's attributes from AWS and heals the record. Otherwise ${touch}.`;
28318
+ switch (outcome.kind) {
28319
+ case "failed": return `cdkd tried to re-read the attributes from AWS to heal the record, but ${this.describeFailureObserved("the provider read", outcome.error, context)}. Fix that (a missing read permission is the usual cause) and deploy again — cdkd retries the read on every deploy until the record is healed — or ${touch}.`;
28320
+ case "not-found": return "cdkd tried to re-read the attributes from AWS to heal the record, but AWS reports no resource behind the recorded physical id — it was probably deleted outside cdkd. Check it with 'cdkd drift', then re-create it (change the resource so it is replaced) or remove it from state.";
28321
+ case "read":
28322
+ if ((outcome.withheldKeys?.length ?? 0) > 0) return this.withheldRemedy();
28323
+ return `cdkd re-read the resource from AWS and the read reports no usable value for this attribute either, so there is nothing to heal the record with; ${touch}.`;
28324
+ case "not-attempted": return `cdkd did not re-read it from AWS (the record was written by this deploy, or this resource type has no read-only lookup); ${touch}.`;
28325
+ }
28326
+ }
28327
+ /**
28328
+ * The remedy half of the "not enriched" refusal. The pre-#1852 sentence,
28329
+ * plus what the heal established: a completed re-read that reports no such
28330
+ * attribute CONFIRMS the type does not supply it; a context with no healer
28331
+ * (`cdkd diff`, `cdkd drift`, ...) re-read nothing, so the record may merely
28332
+ * predate the enrichment and `cdkd deploy` is what heals it.
28333
+ */
28334
+ unenrichedRemedy(resourceType, attributeName, outcome, context) {
28335
+ const fileIssue = `Avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${this.displayMasked(attributeName, context)}.`;
28336
+ if (outcome === void 0) return `If this record was written by an older cdkd that did not record the attribute yet, 'cdkd deploy' re-reads it from AWS and heals the record. ${fileIssue}`;
28337
+ if (outcome.kind === "read") {
28338
+ if (this.healWithheld(outcome, attributeName)) return this.withheldRemedy();
28339
+ return `cdkd re-read the resource's attributes from AWS and the read reports none by that name. ${fileIssue}`;
28340
+ }
28341
+ return fileIssue;
28342
+ }
28343
+ /** Did the heal's read report this attribute (by its top-level key) and cdkd withhold it as masked? */
28344
+ healWithheld(outcome, attributeName) {
28345
+ const head = attributeName.split(".")[0];
28346
+ return outcome.withheldKeys?.some((key) => key === attributeName || key === head) ?? false;
28347
+ }
28348
+ /**
28349
+ * The remedy when the re-read DID report a value and cdkd withheld it: Cloud
28350
+ * Control's read-back is masked wherever cdkd cannot certify a key as a
28351
+ * read-only attribute, which is every key when `DescribeType` is unavailable.
28352
+ * "The read reports none ... file an issue" would be false here.
28353
+ */
28354
+ withheldRemedy() {
28355
+ return "cdkd re-read the resource through Cloud Control, but withheld the value: it could not confirm that this is a read-only attribute of the type, and an unconfirmed value is never used. Grant the deploy role cloudformation:DescribeType and deploy again; if the role already has it, the name is a writable property rather than an attribute — reference the value the template sets instead.";
28356
+ }
28357
+ /**
28358
+ * Ask the context's healer (issue #1852) — `undefined` when it has none. The
28359
+ * healer is contracted never to throw; the `catch` makes that a property of
28360
+ * THIS call site rather than of every supplier, because a throw here would
28361
+ * fail a deploy the pre-#1852 code passed (the warn-and-return fallback).
28362
+ */
28363
+ async healStaleAttributes(logicalId, resource, context) {
28364
+ if (context.attributeHealer === void 0) return void 0;
28365
+ try {
28366
+ return await context.attributeHealer(logicalId, resource);
28367
+ } catch (error) {
28368
+ return {
28369
+ kind: "failed",
28370
+ error
28371
+ };
28372
+ }
28373
+ }
28374
+ /**
28375
+ * The attribute out of a heal's read-back, or `undefined` when the read does
28376
+ * not supply a USABLE one. A value carrying `SECRET_MASK` is not usable:
28377
+ * `CloudControlProvider.import` masks what it cannot certify, and serving
28378
+ * that would send the literal mask to AWS.
28379
+ */
28380
+ usableHealedAttribute(outcome, attributeName) {
28381
+ if (outcome?.kind !== "read") return void 0;
28382
+ const value = readHealedAttribute(outcome.attributes, attributeName);
28383
+ return value === void 0 || carriesSecretMask(value) ? void 0 : value;
28384
+ }
28385
+ /** Serve a value the #1852 heal just read from AWS, logging it like a cached read. */
28386
+ serveHealedAttribute(logicalId, attributeName, value, context) {
28387
+ this.logger.debug(`Resolved Fn::GetAtt from a re-read of AWS (the state record lacked it): ${this.displayMasked(logicalId, context)}.${this.displayMasked(attributeName, context)} -> ${this.displayMasked(stringifyAttributeForLog(attributeName, this.maskValueLeaves(value, context)), context)}`);
28388
+ return this.noteAttributeSecrecy(logicalId, attributeName, value, context);
28389
+ }
28390
+ /**
28391
+ * {@link constructGuardedAttribute}, healing a STALE state record first when
28392
+ * the construction is about to take {@link guardedPhysicalIdFallback} (issue
28393
+ * [#1852](https://github.com/go-to-k/cdkd/issues/1852)).
28394
+ *
28395
+ * Three passes at most, and an AWS call on none of them unless the fallback
28396
+ * is actually reached:
28397
+ *
28398
+ * 1. PROBE — construct under a derived context whose fallback raises
28399
+ * {@link StaleAttributeMissSignal} instead of deciding. Every per-type arm
28400
+ * that CAN answer (the ~40 constructed ARNs, the live-read arms) returns
28401
+ * here exactly as before.
28402
+ * 2. HEAL — only on the signal: ask the context's healer, which re-reads the
28403
+ * record's attributes through its provider once per deploy. A value for
28404
+ * this attribute is served from that read.
28405
+ * 3. SETTLE — otherwise construct again under a context carrying the heal's
28406
+ * outcome, so the fallback decides exactly as it always has (refuse an
28407
+ * `*Arn` / `*Url` shape, refuse under `--strict-getatt`, else warn and
28408
+ * return the physical id) and words a refusal from what was observed.
28409
+ *
28410
+ * The phase rides a DERIVED context, never a field of this resolver: one
28411
+ * resolver instance serves every concurrently resolving resource of a stack.
28412
+ * A context with no healer skips all of it.
28413
+ */
28414
+ async constructWithStaleRecordHeal(resource, attributeName, context, logicalId) {
28415
+ if (context.attributeHealer === void 0) return this.constructGuardedAttribute(resource, attributeName, context, logicalId);
28416
+ try {
28417
+ return await this.constructGuardedAttribute(resource, attributeName, {
28418
+ ...context,
28419
+ staleAttributeHeal: { phase: "probe" }
28420
+ }, logicalId);
28421
+ } catch (error) {
28422
+ if (!(error instanceof StaleAttributeMissSignal)) throw error;
28423
+ }
28424
+ const outcome = await this.healStaleAttributes(logicalId, resource, context) ?? { kind: "not-attempted" };
28425
+ const healed = this.usableHealedAttribute(outcome, attributeName);
28426
+ if (healed !== void 0) return this.serveHealedAttribute(logicalId, attributeName, healed, context);
28427
+ return this.constructGuardedAttribute(resource, attributeName, {
28428
+ ...context,
28429
+ staleAttributeHeal: {
28430
+ phase: "settled",
28431
+ outcome
28432
+ }
28433
+ }, logicalId);
28198
28434
  }
28199
28435
  /**
28200
28436
  * Construct resource attribute value based on resource type, refusing to
@@ -28667,7 +28903,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
28667
28903
  default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
28668
28904
  }
28669
28905
  if (resourceType === "AWS::RDS::DBProxy" || resourceType === "AWS::RDS::DBProxyEndpoint") {
28670
- if (attributeName === "VpcId") throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(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 "${this.displayMasked(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.`));
28906
+ if (attributeName === "VpcId") {
28907
+ if (context.staleAttributeHeal?.phase === "probe") throw new StaleAttributeMissSignal();
28908
+ const vpcIdHealOutcome = context.staleAttributeHeal?.phase === "settled" ? context.staleAttributeHeal.outcome : void 0;
28909
+ throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(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 "${this.displayMasked(physicalId, context)}" is a name, not a VPC id, so cdkd refuses to substitute it. ${this.staleRecordRemedy(vpcIdHealOutcome, context)} Referencing the VPC directly also works.`));
28910
+ }
28671
28911
  return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
28672
28912
  }
28673
28913
  return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
@@ -28769,15 +29009,23 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
28769
29009
  * per-type handlers.
28770
29010
  */
28771
29011
  guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context) {
29012
+ if (context?.staleAttributeHeal?.phase === "probe") throw new StaleAttributeMissSignal();
29013
+ const healOutcome = context?.staleAttributeHeal?.phase === "settled" ? context.staleAttributeHeal.outcome : void 0;
29014
+ const recordMayBeStale = healOutcome?.kind === "failed" || healOutcome?.kind === "not-found";
28772
29015
  const expectsArnShape = attributeName.endsWith("Arn") && !physicalId.startsWith("arn:");
28773
29016
  const expectsUrlShape = attributeName.endsWith("Url") && !/^https?:\/\//.test(physicalId);
28774
29017
  if (expectsArnShape || expectsUrlShape) {
28775
29018
  const expectedShape = expectsArnShape ? "an ARN (arn:...)" : "a URL (http(s)://...)";
28776
- throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(attributeName, context)}] for ${resourceType}: attributes are not enriched for this resource type, and the physical ID fallback "${this.displayMasked(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.displayMasked(attributeName, context)}.`));
29019
+ throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(attributeName, context)}] for ${resourceType}: ` + (recordMayBeStale ? `the state record holds no value for it, and the physical ID ` : `attributes are not enriched for this resource type, and the physical ID `) + `fallback "${this.displayMasked(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). ` + (recordMayBeStale ? this.staleRecordRemedy(healOutcome, context) : this.unenrichedRemedy(resourceType, attributeName, healOutcome, context))));
28777
29020
  }
28778
- if (this.strictGetAtt) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(attributeName, context)}] for ${resourceType}: attributes are not enriched for this resource type, and --strict-getatt rejects the physical ID fallback "${this.displayMasked(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.displayMasked(attributeName, context)}.`));
29021
+ if (this.strictGetAtt) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(attributeName, context)}] for ${resourceType}: ` + (recordMayBeStale ? `the state record holds no value for it, and --strict-getatt ` : `attributes are not enriched for this resource type, and --strict-getatt `) + `rejects the physical ID fallback "${this.displayMasked(physicalId, context)}" (which may not be the value CloudFormation would return). Drop --strict-getatt to fall back with a warning. ` + (recordMayBeStale ? this.staleRecordRemedy(healOutcome, context) : this.unenrichedRemedy(resourceType, attributeName, healOutcome, context))));
28779
29022
  this.physicalIdFallbackCount++;
28780
- this.logger.warn(`Unknown attribute ${this.displayMasked(attributeName, context)} for resource type ${resourceType}, returning physical ID`);
29023
+ if (recordMayBeStale) {
29024
+ this.logger.warn(`The state record for ${this.displayMasked(logicalId, context)} (${resourceType}) holds no ${this.displayMasked(attributeName, context)}, returning physical ID. ` + this.staleRecordRemedy(healOutcome, context));
29025
+ return physicalId;
29026
+ }
29027
+ const withheld = healOutcome?.kind === "read" && this.healWithheld(healOutcome, attributeName) ? `. ${this.withheldRemedy()}` : "";
29028
+ this.logger.warn(`Unknown attribute ${this.displayMasked(attributeName, context)} for resource type ${resourceType}, returning physical ID${withheld}`);
28781
29029
  return physicalId;
28782
29030
  }
28783
29031
  /**
@@ -32887,7 +33135,7 @@ var CloudControlProvider = class {
32887
33135
  const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
32888
33136
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
32889
33137
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
32890
- const { ASGProvider } = await import("./asg-provider-BuEvX2d9.js").then((n) => n.n);
33138
+ const { ASGProvider } = await import("./asg-provider-BTxYWlwu.js").then((n) => n.n);
32891
33139
  const asgProvider = new ASGProvider();
32892
33140
  return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
32893
33141
  }
@@ -36889,6 +37137,11 @@ var ProviderRegistry = class {
36889
37137
  return hits;
36890
37138
  }
36891
37139
  };
37140
+ /**
37141
+ * True for the types `CustomResourceProvider` serves. Exported for
37142
+ * `type-change-guard.ts`'s `equalIdNamesSameResource`: these types are the one
37143
+ * family whose physical ids share ONE namespace across types.
37144
+ */
36892
37145
  function isCustomResource(resourceType) {
36893
37146
  return resourceType.startsWith("Custom::") || resourceType === "AWS::CloudFormation::CustomResource";
36894
37147
  }
@@ -37590,6 +37843,35 @@ function getCurrentResourceSecrets() {
37590
37843
  //#endregion
37591
37844
  //#region src/deployment/type-change-guard.ts
37592
37845
  /**
37846
+ * Does an EQUAL physical id on the two halves of a replacement name the SAME
37847
+ * resource? (issue [#2668](https://github.com/go-to-k/cdkd/issues/2668))
37848
+ *
37849
+ * Within one type, always — which is what the engine's name-idempotent-create
37850
+ * guards and the rollback's "adopted the live new resource" shortcut assume.
37851
+ * Across a `Type` change an equal id is a coincidence of two namespaces (an SSM
37852
+ * parameter and a log group, an IAM user and an IAM group, can share a bare
37853
+ * name): the create was genuine, and the other half still has to be deleted
37854
+ * through its own type's provider.
37855
+ *
37856
+ * The ONE exception is the custom-resource family. Every `Custom::*` type and
37857
+ * `AWS::CloudFormation::CustomResource` are served by the user's handler, which
37858
+ * picks the id, so `Custom::Foo` -> `Custom::Bar` returning the same
37859
+ * `PhysicalResourceId` names the SAME resource — and "deleting the other half"
37860
+ * would send `Delete` for what the create just built. That holds only on the
37861
+ * SDK layer: Cloud Control addresses a resource by type AND identifier.
37862
+ *
37863
+ * Keyed on the TYPES, deliberately not on "both halves resolved to one provider
37864
+ * instance": `register-providers.ts` shares one instance across many types
37865
+ * whose namespaces are disjoint (`AWS::IAM::User` / `AWS::IAM::Group`, the EC2,
37866
+ * RDS, Glue, ECS families ...), and reading those as one namespace refuses a
37867
+ * genuine create on the deploy side and strands the new resource on the
37868
+ * rollback side.
37869
+ */
37870
+ function equalIdNamesSameResource(input) {
37871
+ if (input.oldType === input.newType) return true;
37872
+ return isCustomResource(input.oldType) && isCustomResource(input.newType) && input.createLayer !== "cc-api";
37873
+ }
37874
+ /**
37593
37875
  * The CFn type of a nested stack's row in its PARENT's template.
37594
37876
  *
37595
37877
  * Spelled locally rather than imported, matching
@@ -37597,14 +37879,14 @@ function getCurrentResourceSecrets() {
37597
37879
  * `src/cli/commands/retire-cfn-stack.ts`, and importing a CLI command module
37598
37880
  * from the deployment layer would invert the dependency direction.
37599
37881
  */
37600
- const NESTED_STACK_RESOURCE_TYPE = "AWS::CloudFormation::Stack";
37882
+ const NESTED_STACK_RESOURCE_TYPE$1 = "AWS::CloudFormation::Stack";
37601
37883
  /**
37602
37884
  * Find every planned change whose recorded type and template type differ with
37603
37885
  * `AWS::CloudFormation::Stack` on one side.
37604
37886
  *
37605
- * Reads exactly the two values the defect is made of: `change.resourceType`
37606
- * (what `provisionResource` binds and routes BOTH replacement halves on) and
37607
- * the state record's `resourceType` (the resource that actually exists). That
37887
+ * Reads exactly the two values a replacement's halves route on:
37888
+ * `change.resourceType` (the create) and the state record's `resourceType`
37889
+ * (the resource that actually exists, and the delete). That
37608
37890
  * is deliberate — deriving the "desired" type from the template again would be
37609
37891
  * a second implementation of the diff's own Type-change rule (metadata skip,
37610
37892
  * condition pruning) which could drift away from the routing decision this
@@ -37624,8 +37906,8 @@ function findNestedStackTypeChanges(input) {
37624
37906
  const currentType = currentResource.resourceType;
37625
37907
  const desiredType = change.resourceType;
37626
37908
  if (currentType === desiredType) continue;
37627
- const intoNested = desiredType === NESTED_STACK_RESOURCE_TYPE;
37628
- if (!intoNested && !(currentType === NESTED_STACK_RESOURCE_TYPE)) continue;
37909
+ const intoNested = desiredType === NESTED_STACK_RESOURCE_TYPE$1;
37910
+ if (!intoNested && !(currentType === NESTED_STACK_RESOURCE_TYPE$1)) continue;
37629
37911
  found.push({
37630
37912
  logicalId,
37631
37913
  currentType,
@@ -37637,18 +37919,18 @@ function findNestedStackTypeChanges(input) {
37637
37919
  return found;
37638
37920
  }
37639
37921
  /**
37640
- * Render the refusal. Names the logical id, BOTH types, the resource the
37641
- * mis-routed delete would be aimed at, and what to do instead.
37922
+ * Render the refusal. Names the logical id, BOTH types, the existing resource,
37923
+ * the child stack involved, and what to do instead.
37642
37924
  *
37643
- * `stackName` is the stack being deployed, so the into-nested arm can print the
37644
- * child stack name `NestedStackProvider.delete` would derive and destroy — the
37645
- * one piece of the damage the user cannot read off their own template.
37925
+ * `stackName` is the stack being deployed, so each arm can print the child
37926
+ * stack name `<stackName>~<logicalId>` — the one piece the user cannot read off
37927
+ * their own template.
37646
37928
  */
37647
37929
  function renderNestedStackTypeChangeRefusal(typeChanges, stackName) {
37648
37930
  const rows = typeChanges.map((tc) => {
37649
- return `${` - ${tc.logicalId}: Type changes from ${tc.currentType} to ${tc.desiredType} (the existing ${tc.currentType} is ${tc.physicalId}).`}\n${tc.direction === "into-nested-stack" ? ` Both halves of the replacement would route on the TEMPLATE's type, so the existing resource's delete would be dispatched at the ${NESTED_STACK_RESOURCE_TYPE} provider — which ignores the physical id it is handed and instead destroys the nested child stack "${stackName}~${tc.logicalId}" and every resource that child owns. Where no such child exists the delete is a no-op and ${tc.physicalId} is silently leaked instead.` : ` Both halves of the replacement would route on the TEMPLATE's type, so the existing nested stack's delete would be dispatched at the ${tc.desiredType} provider, which cannot delete a nested stack — the child stack "${stackName}~${tc.logicalId}" and every resource it owns would be left behind, untracked.`}`;
37931
+ return `${` - ${tc.logicalId}: Type changes from ${tc.currentType} to ${tc.desiredType} (the existing ${tc.currentType} is ${tc.physicalId}).`}\n${tc.direction === "into-nested-stack" ? ` Replacing a single resource BY a nested stack is not supported: the create half of the replacement would deploy a whole child stack "${stackName}~${tc.logicalId}" as one row, while ${tc.physicalId} is still recorded under this logical id.` : ` Replacing a nested stack BY a single resource is not supported: a replacement deletes its old half as a best-effort cleanup step whose failure is only a warning, so the child stack "${stackName}~${tc.logicalId}" and every resource it owns could be left behind, untracked, under a deploy that reports success.`}`;
37650
37932
  });
37651
- return `Refusing to deploy ${stackName}: ` + (typeChanges.length === 1 ? `a resource changes its Type ` : `${typeChanges.length} resources change their Type `) + `into or out of ${NESTED_STACK_RESOURCE_TYPE}, which cdkd cannot replace safely (issue #2668).\n${rows.join("\n")}\n Deploy this as two changes instead: give the new resource a DIFFERENT logical id (in CDK, rename the construct) so the existing row is deleted through its own type's provider and the new one is created under its own — or remove the resource in one deploy and add its replacement in the next. There is no flag that overrides this refusal: the delete's TARGET would be wrong, not merely its consequences.`;
37933
+ return `Refusing to deploy ${stackName}: ` + (typeChanges.length === 1 ? `a resource changes its Type ` : `${typeChanges.length} resources change their Type `) + `into or out of ${NESTED_STACK_RESOURCE_TYPE$1}, which cdkd does not replace in place (issue #2668).\n${rows.join("\n")}\n Deploy this as two changes instead: give the new resource a DIFFERENT logical id (in CDK, rename the construct) so the existing row is deleted through its own type's provider and the new one is created under its own — or remove the resource in one deploy and add its replacement in the next. There is no flag that overrides this refusal.`;
37652
37934
  }
37653
37935
 
37654
37936
  //#endregion
@@ -40927,13 +41209,86 @@ const RECREATE_RETRY_SCHEDULE = {
40927
41209
  initialDelayMs: 2e3,
40928
41210
  maxDelayMs: 1e4
40929
41211
  };
41212
+ /** Spelled locally: importing the CLI's copy would invert the layer direction. */
41213
+ const NESTED_STACK_RESOURCE_TYPE = "AWS::CloudFormation::Stack";
41214
+ const nonEmptyString = (v) => typeof v === "string" && v.length > 0;
41215
+ /** The two places a journal can name the old resource's type, each `undefined` when unusable. */
41216
+ function journaledOldTypes(op) {
41217
+ return {
41218
+ stamped: nonEmptyString(op.previousResourceType) ? op.previousResourceType : void 0,
41219
+ recorded: nonEmptyString(op.previousState?.resourceType) ? op.previousState.resourceType : void 0
41220
+ };
41221
+ }
40930
41222
  /**
40931
- * True when the op recorded a replacement (old physical id differs from the
40932
- * new one). The old physical resource is already gone / orphaned, so an
40933
- * in-place revert is best-effort — the plan labels these explicitly.
41223
+ * Which type the OLD half of a replacement op routes on, or why that cannot be
41224
+ * decided (issue [#2668](https://github.com/go-to-k/cdkd/issues/2668)).
41225
+ *
41226
+ * Two sources name it: {@link CompletedOperation.previousResourceType} (written
41227
+ * by a binary that knows about Type changes) and `previousState.resourceType`
41228
+ * (journaled by every binary, inside the previous record). A legacy journal has
41229
+ * only the second, which is enough. The verdict is REFUSED rather than guessed
41230
+ * in three shapes, because a wrong answer dispatches a create and a delete at
41231
+ * the wrong service:
41232
+ *
41233
+ * - neither source names a type (a hand-edited or torn op) — falling back to
41234
+ * `op.resourceType` is exactly the single-type assumption this replaces;
41235
+ * - the two sources disagree — one of them was edited, and nothing says which;
41236
+ * - the types differ with `AWS::CloudFormation::Stack` on either side. The
41237
+ * deploy refuses that pair at plan time (`type-change-guard.ts`), so only a
41238
+ * journal from a binary older than that guard carries one, and what that
41239
+ * deploy left behind is not knowable from the journal: its mis-routed
41240
+ * delete may have destroyed the child it had just created.
41241
+ */
41242
+ function resolveReplacementOldType(op) {
41243
+ const { stamped, recorded } = journaledOldTypes(op);
41244
+ if (stamped !== void 0 && recorded !== void 0 && stamped !== recorded) return {
41245
+ ok: false,
41246
+ reason: `the journal names two different types for the old resource (previousResourceType ${safe(stamped)}, previousState.resourceType ${safe(recorded)})`
41247
+ };
41248
+ const oldType = stamped ?? recorded;
41249
+ if (oldType === void 0) return {
41250
+ ok: false,
41251
+ reason: "the journal does not record the old resource's type"
41252
+ };
41253
+ if (oldType !== op.resourceType && (oldType === NESTED_STACK_RESOURCE_TYPE || op.resourceType === NESTED_STACK_RESOURCE_TYPE)) return {
41254
+ ok: false,
41255
+ reason: `it is a Type change between ${safe(oldType)} and ${safe(op.resourceType)}, and cdkd does not replace a nested stack with, or by, a single resource`
41256
+ };
41257
+ return {
41258
+ ok: true,
41259
+ oldType
41260
+ };
41261
+ }
41262
+ /**
41263
+ * The one refusal both the `refuse-replacement-routing` arm and the
41264
+ * `reverse-replacement` arm's own guard raise. `markNonRetryable`: the verdict
41265
+ * is read off the journal alone, so no retry can change it.
41266
+ */
41267
+ function unroutableReplacementError(op, reason) {
41268
+ return markNonRetryable(new CdkdError(`Cannot reverse the replacement of ${safe(op.logicalId)} (${safe(op.resourceType)}): ${reason}, so cdkd will not guess which provider re-creates the old resource. Nothing was changed. The journal is kept: fix forward with \`cdkd deploy\`, or re-run with ` + (typeof op.logicalId === "string" && PASTEABLE_LOGICAL_ID.test(op.logicalId) ? `\`cdkd rollback --orphan ${op.logicalId}\`` : `\`cdkd rollback --orphan <id>\` (read the id from \`cdkd events\`)`) + ` to leave this resource as it is and let the rest of the rollback proceed.`, "ROLLBACK_REPLACEMENT_UNROUTABLE"));
41269
+ }
41270
+ /**
41271
+ * True when the op changed the resource's `Type`. `false` when the old type is
41272
+ * not recorded at all: that shape is refused by {@link resolveReplacementOldType}
41273
+ * where it matters, and must not by itself turn an in-place op into a
41274
+ * replacement.
41275
+ */
41276
+ function isTypeChangeOp(op) {
41277
+ const { stamped, recorded } = journaledOldTypes(op);
41278
+ return stamped !== void 0 && stamped !== op.resourceType || recorded !== void 0 && recorded !== op.resourceType;
41279
+ }
41280
+ /**
41281
+ * True when the op recorded a replacement: the old physical id differs from the
41282
+ * new one, OR the resource's `Type` changed (issue #2668). The second arm is
41283
+ * not redundant — two types' physical-id namespaces can overlap (a log group
41284
+ * and a Lambda function are both addressed by a bare name), so a Type change
41285
+ * can keep the id, and classified as an in-place `revert` it would hand the
41286
+ * NEW resource's id to an `update()` of either type. The old physical resource
41287
+ * is already gone / orphaned, so an in-place revert is best-effort — the plan
41288
+ * labels these explicitly.
40934
41289
  */
40935
41290
  function isReplacementOp(op) {
40936
- return op.changeType === "UPDATE" && op.previousState?.physicalId !== void 0 && op.previousState.physicalId !== op.physicalId;
41291
+ return op.changeType === "UPDATE" && op.previousState?.physicalId !== void 0 && (op.previousState.physicalId !== op.physicalId || isTypeChangeOp(op));
40937
41292
  }
40938
41293
  function deepEqual(a, b) {
40939
41294
  if (a === b) return true;
@@ -40976,11 +41331,15 @@ function classifyRollbackOp(op, stateResources, orphanLogicalIds) {
40976
41331
  const current = stateResources[op.logicalId];
40977
41332
  if (!current) return "skip-absent";
40978
41333
  if (replacement) {
40979
- if (current.physicalId === op.previousState.physicalId) return "skip-already-done";
41334
+ const oldTypes = journaledOldTypes(op);
41335
+ const oldTypeForCompare = isTypeChangeOp(op) ? oldTypes.stamped ?? oldTypes.recorded : void 0;
41336
+ const sameTypeAsOld = oldTypeForCompare === void 0 || !nonEmptyString(current.resourceType) || current.resourceType === oldTypeForCompare;
41337
+ if (current.physicalId === op.previousState.physicalId && sameTypeAsOld) return "skip-already-done";
40980
41338
  if (op.physicalId !== void 0 && current.physicalId !== op.physicalId) {
40981
- if (deepEqual(current.properties, op.previousState.properties)) return "skip-already-done";
41339
+ if (sameTypeAsOld && deepEqual(current.properties, op.previousState.properties)) return "skip-already-done";
40982
41340
  return "skip-mismatch";
40983
41341
  }
41342
+ if (!resolveReplacementOldType(op).ok) return "refuse-replacement-routing";
40984
41343
  return op.oldResourceRetained ?? op.previousState.updateReplacePolicy === "Retain" ? "reverse-replacement-readopt" : "reverse-replacement";
40985
41344
  }
40986
41345
  if (op.previousState && deepEqual(current.properties, op.previousState.properties)) return "skip-already-done";
@@ -41004,6 +41363,7 @@ function classifyFailedOp(op, stateResources) {
41004
41363
  return "delete-failed-create";
41005
41364
  }
41006
41365
  if (!current || !op.previousState) return "skip-failed-absent";
41366
+ if (isTypeChangeOp(op)) return "skip-failed-type-change";
41007
41367
  return "revert-failed-update";
41008
41368
  }
41009
41369
  /** Build the plan items for a segment's failed ops (issue #1198). */
@@ -41625,6 +41985,10 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
41625
41985
  logger.warn(` Rollback: Cannot restore ${safe(op.logicalId)} — resource no longer in state, skipping`);
41626
41986
  result.warnings++;
41627
41987
  return;
41988
+ case "refuse-replacement-routing": {
41989
+ const routing = resolveReplacementOldType(op);
41990
+ throw unroutableReplacementError(op, routing.ok ? "its old type could not be routed" : routing.reason);
41991
+ }
41628
41992
  case "orphan-flag":
41629
41993
  if (op.changeType === "CREATE") {
41630
41994
  const record = stateResources[op.logicalId];
@@ -41770,6 +42134,10 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
41770
42134
  case "reverse-replacement": {
41771
42135
  const current = stateResources[op.logicalId];
41772
42136
  const prev = op.previousState;
42137
+ const oldTypeRouting = resolveReplacementOldType(op);
42138
+ if (!oldTypeRouting.ok) throw unroutableReplacementError(op, oldTypeRouting.reason);
42139
+ const oldType = oldTypeRouting.oldType;
42140
+ const typeChanged = oldType !== op.resourceType;
41773
42141
  if (!requireRestorableBaseline(prev.properties, logger, {
41774
42142
  logicalId: op.logicalId,
41775
42143
  consequence: "create a default-configured resource and then delete the live one",
@@ -41781,14 +42149,14 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
41781
42149
  }
41782
42150
  const resolvedPrevProps = await resolveReplayProps(prev.properties, resolver, secrets, ctx, op.logicalId) ?? {};
41783
42151
  refuseMaskedReplayBaseline(resolvedPrevProps, op.logicalId);
41784
- recordNestedStackParameterExpressions(secrets, op.resourceType, resolvedPrevProps, prev.properties, STATE_DERIVED_RULES);
41785
- logger.info(` Rollback: Reversing replacement of ${safe(op.logicalId)} (${safe(op.resourceType)}) — re-creating the old resource and deleting the new one`);
41786
- 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.`);
42152
+ recordNestedStackParameterExpressions(secrets, oldType, resolvedPrevProps, prev.properties, STATE_DERIVED_RULES);
42153
+ logger.info(` Rollback: Reversing replacement of ${safe(op.logicalId)} (${typeChanged ? `${safe(op.resourceType)} -> ${safe(oldType)}` : safe(op.resourceType)}) — re-creating the old resource and deleting the new one`);
42154
+ if (STATEFUL_TYPES.has(oldType)) logger.warn(` ⚠ ${safe(op.logicalId)} (${safe(oldType)}) 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.`);
41787
42155
  const { provider: createProvider, provisionedBy: createProvisionedBy } = ctx.providerRegistry.getProviderFor({
41788
- resourceType: op.resourceType,
42156
+ resourceType: oldType,
41789
42157
  provisionedBy: prev.provisionedBy
41790
42158
  });
41791
- const replayCreateProps = () => ({ ...createProvisionedBy === "cc-api" ? applyDefaultNameForFallback(op.logicalId, op.resourceType, resolvedPrevProps) : resolvedPrevProps });
42159
+ const replayCreateProps = () => ({ ...createProvisionedBy === "cc-api" ? applyDefaultNameForFallback(op.logicalId, oldType, resolvedPrevProps) : resolvedPrevProps });
41792
42160
  const resolveNewDeleteProvider = () => ctx.providerRegistry.getProviderFor({
41793
42161
  resourceType: op.resourceType,
41794
42162
  provisionedBy: current.provisionedBy ?? op.provisionedBy
@@ -41796,7 +42164,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
41796
42164
  let deletedNewFirst = false;
41797
42165
  let createResult;
41798
42166
  try {
41799
- createResult = await createWithRollbackRetry(createProvider, () => withCurrentResourceSecrets(secrets, () => createProvider.create(op.logicalId, op.resourceType, replayCreateProps(), replayingStateCreateContext(secrets))), op.logicalId, logger, isInterrupted, secrets, {
42167
+ createResult = await createWithRollbackRetry(createProvider, () => withCurrentResourceSecrets(secrets, () => createProvider.create(op.logicalId, oldType, replayCreateProps(), replayingStateCreateContext(secrets))), op.logicalId, logger, isInterrupted, secrets, {
41800
42168
  isRetryable: isNameCooldownError,
41801
42169
  interruptedMessage: "Rollback interrupted while waiting out the name cooldown"
41802
42170
  });
@@ -41804,7 +42172,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
41804
42172
  const msg = createError instanceof Error ? createError.message : String(createError);
41805
42173
  if (!isNameCollisionErrorFrom(createError, op.logicalId)) throw createError;
41806
42174
  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)));
41807
- logger.info(` Rollback: re-create collided with the new resource's name — deleting the new resource (${displaySafe(current.physicalId)}) first...`);
42175
+ logger.info(` Rollback: re-create collided with the new resource's name — deleting the new resource (${displaySafe(current.physicalId)}) first...` + (typeChanged ? ` (this op changed the resource's Type, ${safe(op.resourceType)} -> ${safe(oldType)}: if those types do not share a name space the name is held by an unrelated resource and the re-create will collide again)` : ""));
41808
42176
  {
41809
42177
  const finalSnapshotIdentifier = rollbackFinalSnapshotId(op.resourceType, current, op.provisionedBy);
41810
42178
  throwIfDeleteSkipped(await resolveNewDeleteProvider().delete(op.logicalId, current.physicalId, op.resourceType, current.properties, {
@@ -41816,7 +42184,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
41816
42184
  delete stateResources[op.logicalId];
41817
42185
  await afterOp?.(op.logicalId);
41818
42186
  try {
41819
- createResult = await createWithRollbackRetry(createProvider, () => withCurrentResourceSecrets(secrets, () => createProvider.create(op.logicalId, op.resourceType, replayCreateProps(), replayingStateCreateContext(secrets))), op.logicalId, logger, isInterrupted, secrets, {
42187
+ createResult = await createWithRollbackRetry(createProvider, () => withCurrentResourceSecrets(secrets, () => createProvider.create(op.logicalId, oldType, replayCreateProps(), replayingStateCreateContext(secrets))), op.logicalId, logger, isInterrupted, secrets, {
41820
42188
  isRetryable: isRecreateRetryableError,
41821
42189
  interruptedMessage: "Rollback interrupted while waiting for the old name to release"
41822
42190
  });
@@ -41824,7 +42192,11 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
41824
42192
  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) });
41825
42193
  }
41826
42194
  }
41827
- const adoptedLiveNewResource = !deletedNewFirst && createResult.physicalId === current.physicalId;
42195
+ const adoptedLiveNewResource = equalIdNamesSameResource({
42196
+ oldType,
42197
+ newType: op.resourceType,
42198
+ createLayer: createProvisionedBy
42199
+ }) && !deletedNewFirst && createResult.physicalId === current.physicalId;
41828
42200
  if (adoptedLiveNewResource) {
41829
42201
  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.`);
41830
42202
  result.warnings++;
@@ -42004,6 +42376,10 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
42004
42376
  logger.warn(` Rollback: cannot revert failed UPDATE of ${safe(op.logicalId)} — no previous state available, skipping`);
42005
42377
  result.warnings++;
42006
42378
  break;
42379
+ case "skip-failed-type-change":
42380
+ logger.warn(` Rollback: cannot revert failed UPDATE of ${safe(op.logicalId)} in place — it was a Type change (${safe(op.previousState?.resourceType)} -> ${safe(op.resourceType)}), which is a replacement, and its remote state is unknown. Inspect it with \`cdkd drift\` and re-converge with \`cdkd deploy\`. Skipping.`);
42381
+ result.warnings++;
42382
+ break;
42007
42383
  case "orphan-failed-create-retain": {
42008
42384
  const failedCreateRecord = stateResources[op.logicalId];
42009
42385
  const orphanProvisionedBy = effectiveProvisionedBy(failedCreateRecord, op.provisionedBy);
@@ -43110,6 +43486,40 @@ var DeployEngine = class DeployEngine {
43110
43486
  */
43111
43487
  retainedOldOnReplacement = /* @__PURE__ */ new Set();
43112
43488
  /**
43489
+ * The pre-deploy state records, as loaded — the #1852 heal's eligibility
43490
+ * baseline. A record is healed only while it is still the one this deploy
43491
+ * LOADED (same physical id, same `attributes` object): once a provider has
43492
+ * (re)written it this run, a missing attribute is the provider's answer, not
43493
+ * staleness, and re-reading would cost an AWS call to learn nothing.
43494
+ *
43495
+ * Compared by the `attributes` REFERENCE rather than the record's, because
43496
+ * the metadata-only arm re-spreads a record (`{ ...currentResource }`) without
43497
+ * a provider call — its attributes object survives the spread, a create /
43498
+ * update result's does not.
43499
+ */
43500
+ healBaseline = {};
43501
+ /**
43502
+ * Single-flight + per-deploy memo of the #1852 heal, keyed by logical id and
43503
+ * physical id. ENGINE-instance state, never module-global: one engine deploys
43504
+ * one stack, and `--stack-concurrency` runs several engines at once. Holding
43505
+ * the PROMISE is what makes it single-flight — every concurrent resolution of
43506
+ * the same record awaits the one read — and never deleting an entry is what
43507
+ * bounds it: one read per record per deploy, success or failure, no retry.
43508
+ */
43509
+ attributeHeals = /* @__PURE__ */ new Map();
43510
+ /**
43511
+ * What the heals of this deploy read, waiting for the next state save.
43512
+ *
43513
+ * Applied at {@link redactStateForPersist} — the choke point EVERY save
43514
+ * passes through — rather than written into the in-memory record, so a heal
43515
+ * survives every exit path that saves at all (success, a failed resource, a
43516
+ * failed output, the no-change path), whichever copy-on-write record map the
43517
+ * save happens to hold, and is scrubbed by the same redaction every other
43518
+ * attribute goes through. A path that saves NOTHING (`--dry-run`) persists
43519
+ * nothing and the next deploy re-heals.
43520
+ */
43521
+ healedAttributes = /* @__PURE__ */ new Map();
43522
+ /**
43113
43523
  * Target region for this stack. Required — load-bearing for the
43114
43524
  * region-prefixed S3 state key and recorded in state.json for
43115
43525
  * cross-region destroy.
@@ -43153,6 +43563,9 @@ var DeployEngine = class DeployEngine {
43153
43563
  this.outputsSourceUsable = true;
43154
43564
  this.skippedOutputs = void 0;
43155
43565
  this.retainedOldOnReplacement = /* @__PURE__ */ new Set();
43566
+ this.healBaseline = {};
43567
+ this.attributeHeals = /* @__PURE__ */ new Map();
43568
+ this.healedAttributes = /* @__PURE__ */ new Map();
43156
43569
  this.resolver.resetPhysicalIdFallbackCount();
43157
43570
  return withStackName(stackName, () => this.doDeploy(stackName, template));
43158
43571
  }
@@ -43175,6 +43588,7 @@ var DeployEngine = class DeployEngine {
43175
43588
  ...this.exportIndexStore && { exportIndex: this.exportIndexStore },
43176
43589
  recordedImports: this.recordedImports,
43177
43590
  recordedOutputReads: this.recordedOutputReads,
43591
+ attributeHealer: (logicalId, resource) => this.healStaleAttributes(logicalId, resource, stackName),
43178
43592
  ...this.options.inheritedSecrets && this.options.inheritedSecrets.size > 0 && { inheritedSecrets: this.options.inheritedSecrets },
43179
43593
  recordedSecretValues,
43180
43594
  noEchoAttributeResources: this.noEchoAttributeResources,
@@ -43618,12 +44032,111 @@ var DeployEngine = class DeployEngine {
43618
44032
  return next;
43619
44033
  });
43620
44034
  }
44035
+ /**
44036
+ * Re-read a STALE record's attributes from AWS (issue
44037
+ * [#1852](https://github.com/go-to-k/cdkd/issues/1852)) — the resolver calls
44038
+ * this, through `ResolverContext.attributeHealer`, only when `Fn::GetAtt` is
44039
+ * about to take the physical-id fallback.
44040
+ *
44041
+ * The read is the provider's `import()` with `knownPhysicalId` — the same
44042
+ * primitive `orphan-adoption.ts` verifies a record with. It is READ-ONLY by
44043
+ * contract ("verify the resource exists and fetch attributes, do NOT
44044
+ * search"), it returns the map `create()` records (so a healed record looks
44045
+ * like a fresh one), and it is routed by the record's own `resourceType` +
44046
+ * `provisionedBy`, so a Cloud-Control-routed record is read through Cloud
44047
+ * Control. `getAttribute()` was the alternative and is implemented by about
44048
+ * half the providers — neither of the two types this issue names.
44049
+ *
44050
+ * NEVER throws and never retries: every failure is an outcome the resolver
44051
+ * words its refusal from, and the memo makes one read per record per deploy
44052
+ * the ceiling.
44053
+ */
44054
+ healStaleAttributes(logicalId, resource, stackName) {
44055
+ if (!this.isHealEligible(logicalId, resource)) return Promise.resolve({ kind: "not-attempted" });
44056
+ const key = `${logicalId}\u0000${resource.physicalId}`;
44057
+ const inFlight = this.attributeHeals.get(key);
44058
+ if (inFlight) return inFlight;
44059
+ const heal = this.readStaleAttributes(logicalId, resource, stackName).catch((error) => ({
44060
+ kind: "failed",
44061
+ error
44062
+ }));
44063
+ this.attributeHeals.set(key, heal);
44064
+ return heal;
44065
+ }
44066
+ /**
44067
+ * Is `resource` still the record this deploy LOADED — same physical id, same
44068
+ * `attributes` object — and of a type whose attributes are an AWS read-back?
44069
+ * See {@link healBaseline}. Asked by the read AND by the persist merge: a
44070
+ * read taken before this deploy rewrote the record must reach neither a later
44071
+ * resolution nor the rewritten record.
44072
+ */
44073
+ isHealEligible(logicalId, resource) {
44074
+ const loaded = Object.hasOwn(this.healBaseline, logicalId) ? this.healBaseline[logicalId] : void 0;
44075
+ return loaded !== void 0 && loaded.physicalId === resource.physicalId && loaded.attributes === resource.attributes && !isHealExcludedType(resource.resourceType);
44076
+ }
44077
+ async readStaleAttributes(logicalId, resource, stackName) {
44078
+ const { provider } = this.providerRegistry.getProviderFor({
44079
+ resourceType: resource.resourceType,
44080
+ properties: resource.properties,
44081
+ provisionedBy: resource.provisionedBy
44082
+ });
44083
+ if (!provider.import) return { kind: "not-attempted" };
44084
+ const found = await provider.import({
44085
+ logicalId,
44086
+ resourceType: resource.resourceType,
44087
+ stackName,
44088
+ region: this.stackRegion,
44089
+ properties: resource.properties,
44090
+ knownPhysicalId: resource.physicalId
44091
+ });
44092
+ if (found === null) return { kind: "not-found" };
44093
+ if (found.physicalId !== resource.physicalId) throw new Error(`the provider answered for a different resource (${found.physicalId}) than the one asked about (${resource.physicalId})`);
44094
+ const reported = Object.entries(normalizeHealedAttributes(found.attributes));
44095
+ const attributes = Object.fromEntries(reported.filter(([, value]) => !carriesSecretMask(value)));
44096
+ const withheldKeys = reported.filter(([, value]) => carriesSecretMask(value)).map(([key]) => key);
44097
+ if (Object.keys(attributes).length > 0) this.healedAttributes.set(logicalId, {
44098
+ physicalId: resource.physicalId,
44099
+ resourceType: resource.resourceType,
44100
+ attributes
44101
+ });
44102
+ this.logger.debug(`Re-read the attributes of ${displaySafe(logicalId)} (${displaySafe(resource.resourceType)}) from AWS — its state record lacked one a Fn::GetAtt asked for (#1852): ${Object.keys(attributes).length} attribute(s) read`);
44103
+ return {
44104
+ kind: "read",
44105
+ attributes,
44106
+ ...withheldKeys.length > 0 && { withheldKeys }
44107
+ };
44108
+ }
44109
+ /**
44110
+ * The record to persist for `logicalId`: `record` itself, or a copy whose
44111
+ * `attributes` gained what this deploy's heal read. MERGED, never replaced —
44112
+ * only `attributes` is touched, and within it only keys the record does not
44113
+ * hold (or holds as a pre-#1681 placeholder ARN). Skipped when the record no
44114
+ * longer describes the resource that was read: a replacement or a Type change
44115
+ * this deploy made.
44116
+ */
44117
+ withHealedAttributes(logicalId, record) {
44118
+ const healed = this.healedAttributes.get(logicalId);
44119
+ if (healed === void 0 || healed.physicalId !== record.physicalId || healed.resourceType !== record.resourceType || !this.isHealEligible(logicalId, record)) return record;
44120
+ const merged = mergeHealedAttributes(record.attributes, healed.attributes, (key, value) => isStalePlaceholderArnAttribute(record.resourceType, key, value));
44121
+ return merged === record.attributes || merged === void 0 ? record : {
44122
+ ...record,
44123
+ attributes: merged
44124
+ };
44125
+ }
44126
+ /** Would a save of `resources` persist something a heal read? The no-change path's trigger. */
44127
+ hasUnpersistedHeals(resources) {
44128
+ for (const logicalId of this.healedAttributes.keys()) {
44129
+ const record = Object.hasOwn(resources, logicalId) ? resources[logicalId] : void 0;
44130
+ if (record !== void 0 && this.withHealedAttributes(logicalId, record) !== record) return true;
44131
+ }
44132
+ return false;
44133
+ }
43621
44134
  redactStateForPersist(state) {
43622
44135
  const resources = {};
43623
44136
  for (const [logicalId, record] of Object.entries(state.resources)) {
43624
44137
  const secrets = this.perResourceSecrets.get(logicalId);
43625
44138
  const templateProps = this.perResourceTemplateProps.get(logicalId);
43626
- resources[logicalId] = scrubResourceRecord(record, secrets ?? /* @__PURE__ */ new Map(), templateProps);
44139
+ resources[logicalId] = scrubResourceRecord(this.withHealedAttributes(logicalId, record), secrets ?? /* @__PURE__ */ new Map(), templateProps);
43627
44140
  }
43628
44141
  const orphans = state.orphans?.map((entry) => {
43629
44142
  const sameResource = this.perResourceResolvedType.get(entry.logicalId) === entry.state.resourceType;
@@ -43975,6 +44488,7 @@ var DeployEngine = class DeployEngine {
43975
44488
  lastModified: Date.now()
43976
44489
  };
43977
44490
  const currentEtag = currentStateData?.etag;
44491
+ this.healBaseline = currentState.resources ?? {};
43978
44492
  refuseMalformedOutputs(currentState, stackName, this.stackRegion);
43979
44493
  refuseMalformedResourcesForDeploy(currentState, stackName, this.stackRegion);
43980
44494
  const migrationPending = currentStateData?.migrationPending ?? false;
@@ -44067,7 +44581,8 @@ var DeployEngine = class DeployEngine {
44067
44581
  if (merge?.kind === "kept" && !outputMapsEqual(persistedOutputs, resolvedOutputs)) this.logger.warn(`Outputs changed but one or more could not be resolved; keeping the previously persisted outputs. ${keptWholeReasonText(merge.reason)} A downstream Fn::ImportValue may fail until the next deploy.`);
44068
44582
  else if (merge?.kind === "merged" && merge.carriedKeys.length > 0) this.logger.debug(`Kept the previously persisted value of ${merge.carriedKeys.length} output key(s), carried export aliases included, that could not be resolved (no-change path, #2771)`);
44069
44583
  const skippedOutputsChanged = !skippedOutputsEqual(currentState.skippedOutputs, this.skippedOutputs);
44070
- if (observedRefresh || outputsChanged || exportSetChanged || skippedOutputsChanged || orphansChanged) try {
44584
+ const healedAttributesPending = this.hasUnpersistedHeals(currentState.resources);
44585
+ if (observedRefresh || outputsChanged || exportSetChanged || skippedOutputsChanged || orphansChanged || healedAttributesPending) try {
44071
44586
  const refreshedState = {
44072
44587
  version: 10,
44073
44588
  region: this.stackRegion,
@@ -44093,6 +44608,7 @@ var DeployEngine = class DeployEngine {
44093
44608
  outputs: this.redactOutputs(refreshedState.outputs)
44094
44609
  }));
44095
44610
  } else if (observedRefresh) this.logger.debug("Persisted refreshed observedProperties (no-change path)");
44611
+ else if (healedAttributesPending) this.logger.debug("Persisted attributes re-read from AWS for a stale record (no-change path, #1852)");
44096
44612
  else this.logger.debug("Persisted skipped-outputs record (no outputs-value diff, no-change path, #2740)");
44097
44613
  } catch (saveError) {
44098
44614
  this.logger.warn(`Failed to persist no-change state update: ${saveError instanceof Error ? saveError.message : String(saveError)} — drift baseline / outputs will be re-resolved on next deploy.`);
@@ -44275,7 +44791,8 @@ var DeployEngine = class DeployEngine {
44275
44791
  previousState,
44276
44792
  physicalId: newResources[logicalId]?.physicalId,
44277
44793
  properties: newResources[logicalId]?.properties,
44278
- ...change.changeType === "UPDATE" && { oldResourceRetained: this.retainedOldOnReplacement.has(logicalId) }
44794
+ ...change.changeType === "UPDATE" && { oldResourceRetained: this.retainedOldOnReplacement.has(logicalId) },
44795
+ ...change.changeType === "UPDATE" && previousState !== void 0 && { previousResourceType: previousState.resourceType }
44279
44796
  });
44280
44797
  saveStateAfterResource(logicalId);
44281
44798
  }, () => this.interrupted);
@@ -44656,7 +45173,8 @@ var DeployEngine = class DeployEngine {
44656
45173
  const resourceType = change.resourceType;
44657
45174
  const renderer = getLiveRenderer();
44658
45175
  const labelRecreateDirection = this.recreateDirectionFor(stackName, logicalId);
44659
- const needsReplacement = change.changeType === "UPDATE" && (change.propertyChanges?.some((pc) => pc.requiresReplacement) ?? false) || labelRecreateDirection !== void 0;
45176
+ const labelRecordedType = stateResources[logicalId]?.resourceType;
45177
+ const needsReplacement = change.changeType === "UPDATE" && ((change.propertyChanges?.some((pc) => pc.requiresReplacement) ?? false) || labelRecordedType !== void 0 && labelRecordedType !== resourceType) || labelRecreateDirection !== void 0;
44660
45178
  const verb = change.changeType === "CREATE" ? "Creating" : change.changeType === "DELETE" ? "Deleting" : needsReplacement ? "Replacing" : "Updating";
44661
45179
  const labelRouting = this.peekRoutingForLabel(change, stateResources[logicalId], stackName, logicalId, needsReplacement, labelRecreateDirection);
44662
45180
  const baseLabel = `${verb} ${logicalId} (${resourceType})${labelRouting === "cc-api" ? " [CC API]" : ""}`;
@@ -44883,12 +45401,12 @@ var DeployEngine = class DeployEngine {
44883
45401
  * #960 follow-up) and the name-idempotent same-id guard (issue #1238) so
44884
45402
  * the two --replace escape hatches cannot drift apart.
44885
45403
  */
44886
- async replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, secrets, updateReplacePolicy) {
45404
+ async replaceDeleteFirstAndRecreate(logicalId, resourceType, oldResourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, secrets, updateReplacePolicy) {
44887
45405
  const createContext = { maskSecrets: createSecretMasker(secrets) };
44888
- const finalSnapshotIdentifier = await this.prepareFinalSnapshotForDelete(logicalId, resourceType, currentResource, updateReplacePolicy);
45406
+ const finalSnapshotIdentifier = await this.prepareFinalSnapshotForDelete(logicalId, oldResourceType, currentResource, updateReplacePolicy);
44889
45407
  let deleteResult;
44890
45408
  try {
44891
- deleteResult = await oldDeleteProvider.delete(logicalId, currentResource.physicalId, resourceType, currentResource.properties, {
45409
+ deleteResult = await oldDeleteProvider.delete(logicalId, currentResource.physicalId, oldResourceType, currentResource.properties, {
44892
45410
  expectedRegion: this.stackRegion,
44893
45411
  forceDataDelete: this.options.forceStatefulRecreation === true,
44894
45412
  ...finalSnapshotIdentifier !== void 0 && { finalSnapshotIdentifier }
@@ -44974,6 +45492,8 @@ var DeployEngine = class DeployEngine {
44974
45492
  case "UPDATE": {
44975
45493
  const currentResource = existingState;
44976
45494
  if (!currentResource) throw new Error(`Cannot update ${logicalId}: resource not found in state`);
45495
+ const oldResourceType = currentResource.resourceType;
45496
+ const typeChanged = oldResourceType !== resourceType;
44977
45497
  const desiredProps = change.desiredProperties || {};
44978
45498
  const currentProps = change.currentProperties || {};
44979
45499
  const currentPropsAsWritten = currentResource.provisionedBy === "cc-api" ? currentProps : withoutSilentDropProperties(resourceType, currentProps);
@@ -44996,7 +45516,7 @@ var DeployEngine = class DeployEngine {
44996
45516
  const desiredForSkipCheck = redactSecretsForState(markSameGenerationBag({ ...resolvedProps }), updateSecrets, desiredProps);
44997
45517
  const allowedSilentDrops = this.providerRegistry.getAllowedUnsupportedProperties?.();
44998
45518
  const desiredForSkipCheckAsWritten = currentResource.provisionedBy !== "cc-api" && allowedSilentDrops ? withoutAcceptedSilentDropProperties(resourceType, desiredForSkipCheck, allowedSilentDrops) : desiredForSkipCheck;
44999
- if (JSON.stringify(desiredForSkipCheckAsWritten) === JSON.stringify(currentPropsAsWritten)) {
45519
+ if (!typeChanged && JSON.stringify(desiredForSkipCheckAsWritten) === JSON.stringify(currentPropsAsWritten)) {
45000
45520
  if (change.attributeChanges && change.attributeChanges.length > 0) {
45001
45521
  const attrSummary = change.attributeChanges.map((a) => `${a.attribute}: ${a.oldValue ?? "(unset)"} → ${a.newValue ?? "(unset)"}`).join(", ");
45002
45522
  this.logger.info(` ↻ ${logicalId} (${resourceType}) attribute update: ${attrSummary}`);
@@ -45015,7 +45535,7 @@ var DeployEngine = class DeployEngine {
45015
45535
  if (counts) counts.skipped++;
45016
45536
  break;
45017
45537
  }
45018
- const propertyDrivenReplacement = change.propertyChanges?.some((pc) => pc.requiresReplacement);
45538
+ const propertyDrivenReplacement = typeChanged || change.propertyChanges?.some((pc) => pc.requiresReplacement);
45019
45539
  const recreateTargets = this.options.recreateTargets?.stackName === stackName ? this.options.recreateTargets : void 0;
45020
45540
  const recreateViaCcApi = recreateTargets?.viaCcApi.has(logicalId) ?? false;
45021
45541
  const recreateViaSdkProvider = recreateTargets?.viaSdkProvider.has(logicalId) ?? false;
@@ -45025,17 +45545,17 @@ var DeployEngine = class DeployEngine {
45025
45545
  const updateReplacePolicy = template?.Resources?.[logicalId]?.UpdateReplacePolicy;
45026
45546
  if (needsReplacement) {
45027
45547
  if (propertyDrivenReplacement && !recreateFlagged && updateReplacePolicy !== "Retain") {
45028
- const statefulReason = isStatefulRecreateTargetForReplace(resourceType, currentProps, currentResource.observedProperties);
45548
+ const statefulReason = isStatefulRecreateTargetForReplace(oldResourceType, currentProps, currentResource.observedProperties);
45029
45549
  if (statefulReason && this.options.forceStatefulRecreation !== true) {
45030
- const immutableProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
45031
- throw markNonRetryable(new CdkdError(`${logicalId} (${resourceType}) requires replacement (immutable property changed: ${immutableProps}) but it is a stateful resource — ${renderStatefulReason(statefulReason)}. Re-run with --force-stateful-recreation to confirm the data loss, or change the resource definition to avoid the immutable-property change.`, "STATEFUL_REPLACE_BLOCKED"));
45550
+ const immutableProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ") || "Type";
45551
+ throw markNonRetryable(new CdkdError(`${logicalId} (${oldResourceType}) requires replacement (immutable property changed: ${immutableProps}${typeChanged ? `, to ${resourceType}` : ""}) but it is a stateful resource — ${renderStatefulReason(statefulReason)}. Re-run with --force-stateful-recreation to confirm the data loss, or change the resource definition to avoid the immutable-property change.`, "STATEFUL_REPLACE_BLOCKED"));
45032
45552
  }
45033
45553
  }
45034
45554
  let replacementReason;
45035
45555
  if (recreateViaCcApi) replacementReason = "--recreate-via-cc-api flag (mid-life SDK→CC migration)";
45036
45556
  else if (recreateViaSdkProvider) replacementReason = "--recreate-via-sdk-provider flag (mid-life CC→SDK migration)";
45037
45557
  else replacementReason = `immutable properties changed: ${change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ")}`;
45038
- this.logger.info(`Replacing ${logicalId} (${resourceType}) - ${replacementReason}`);
45558
+ this.logger.info(`Replacing ${logicalId} (${typeChanged ? `${oldResourceType} -> ${resourceType}` : resourceType}) - ${replacementReason}`);
45039
45559
  const recreateDirectionHint = recreateViaCcApi ? "cc-api" : recreateViaSdkProvider ? "sdk" : void 0;
45040
45560
  const replaceDecision = this.providerRegistry.getProviderFor({
45041
45561
  resourceType,
@@ -45046,9 +45566,14 @@ var DeployEngine = class DeployEngine {
45046
45566
  const replaceProvider = replaceDecision.provider;
45047
45567
  const replaceProps = replaceDecision.provisionedBy === "cc-api" ? this.preparePropertiesForCcApi(resourceType, resolvedProps, logicalId) : resolvedProps;
45048
45568
  const oldDeleteProvider = this.providerRegistry.getProviderFor({
45049
- resourceType,
45569
+ resourceType: oldResourceType,
45050
45570
  provisionedBy: currentResource.provisionedBy
45051
45571
  }).provider;
45572
+ const equalIdIsSameResource = equalIdNamesSameResource({
45573
+ oldType: oldResourceType,
45574
+ newType: resourceType,
45575
+ createLayer: replaceDecision.provisionedBy
45576
+ });
45052
45577
  let createResult;
45053
45578
  if (recreateFlagged) {
45054
45579
  const recreateFlagName = recreateViaCcApi ? "--recreate-via-cc-api" : "--recreate-via-sdk-provider";
@@ -45057,10 +45582,10 @@ var DeployEngine = class DeployEngine {
45057
45582
  this.logger.warn(` ⚠ ${logicalId} has UpdateReplacePolicy: Retain — ${recreateFlagName} will leak the old physical resource (${currentResource.physicalId}). The new resource shares the same name where applicable; if the type has user-supplied names (e.g. functionName, bucketName), the create will deterministically collide with the retained orphan.`);
45058
45583
  } else {
45059
45584
  this.logger.info(` Destroying old ${logicalId} (${currentResource.physicalId}) before recreate...`);
45060
- const recreateFinalSnapshotId = await this.prepareFinalSnapshotForDelete(logicalId, resourceType, currentResource, updateReplacePolicy);
45585
+ const recreateFinalSnapshotId = await this.prepareFinalSnapshotForDelete(logicalId, oldResourceType, currentResource, updateReplacePolicy);
45061
45586
  let recreateDeleteResult;
45062
45587
  try {
45063
- recreateDeleteResult = await oldDeleteProvider.delete(logicalId, currentResource.physicalId, resourceType, currentResource.properties, {
45588
+ recreateDeleteResult = await oldDeleteProvider.delete(logicalId, currentResource.physicalId, oldResourceType, currentResource.properties, {
45064
45589
  expectedRegion: this.stackRegion,
45065
45590
  forceDataDelete: this.options.forceStatefulRecreation === true,
45066
45591
  ...recreateFinalSnapshotId !== void 0 && { finalSnapshotIdentifier: recreateFinalSnapshotId }
@@ -45082,7 +45607,7 @@ var DeployEngine = class DeployEngine {
45082
45607
  onInterrupted: () => new InterruptedError(this.interruptCause ?? "user"),
45083
45608
  isRetryable: isRecreateRetryableError
45084
45609
  });
45085
- if (updateReplacePolicy === "Retain" && createResult.physicalId === currentResource.physicalId) throw new CdkdError(`${logicalId} (${resourceType}) recreate returned the existing resource (${currentResource.physicalId}) instead of creating a new one — its Create API is name-idempotent — and UpdateReplacePolicy: Retain means the old resource was never destroyed, so the new properties were not applied. Rename the resource in your CDK code (or remove the explicit physical name) so the recreate can produce a genuinely new resource.`, "NAMED_REPLACEMENT_IDEMPOTENT_CREATE");
45610
+ if (equalIdIsSameResource && updateReplacePolicy === "Retain" && createResult.physicalId === currentResource.physicalId) throw new CdkdError(`${logicalId} (${resourceType}) recreate returned the existing resource (${currentResource.physicalId}) instead of creating a new one — its Create API is name-idempotent — and UpdateReplacePolicy: Retain means the old resource was never destroyed, so the new properties were not applied. Rename the resource in your CDK code (or remove the explicit physical name) so the recreate can produce a genuinely new resource.`, "NAMED_REPLACEMENT_IDEMPOTENT_CREATE");
45086
45611
  } else {
45087
45612
  this.logger.info(` Creating new ${logicalId}...`);
45088
45613
  let deletedOldFirst = false;
@@ -45092,19 +45617,20 @@ var DeployEngine = class DeployEngine {
45092
45617
  const createMsg = createError instanceof Error ? createError.message : String(createError);
45093
45618
  if (!isNameCollisionErrorFrom(createError, logicalId)) throw createError;
45094
45619
  const nameOrigin = this.replacementNameOrigin(logicalId, currentResource.physicalId);
45095
- if (updateReplacePolicy === "Retain") throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but its physical name is still held by the existing resource AND UpdateReplacePolicy: Retain pins that resource in place. ${nameOrigin.descriptor}. ${nameOrigin.remedy} — with Retain, the old resource keeps the name, so a same-name replacement can never proceed.`, "NAMED_REPLACEMENT_COLLISION");
45096
- if (this.options.replace !== true) throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but the create-first attempt collided with the existing resource: ${createMsg}. ${nameOrigin.descriptor}, so the CloudFormation-style safe replacement order (create the new resource before deleting the old) cannot reuse the occupied name — CloudFormation refuses this shape with "cannot update a stack when a custom-named resource requires replacing". ${nameOrigin.remedy}, or re-run with \`cdkd deploy --replace\` to delete the old resource FIRST and recreate it under the same name (the resource is briefly unavailable while it is recreated).`, "NAMED_REPLACEMENT_COLLISION");
45620
+ const typeChangeCollisionNote = typeChanged ? ` Note: this replacement changes the resource's Type (${oldResourceType} -> ${resourceType}). Unless those two types share one name space, the name is held by an unrelated existing ${resourceType}, not by the ${oldResourceType} being replaced — then deleting the old resource first cannot free it, and the fix is a different name.` : "";
45621
+ if (updateReplacePolicy === "Retain") throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but its physical name is still held by the existing resource AND UpdateReplacePolicy: Retain pins that resource in place. ${nameOrigin.descriptor}. ${nameOrigin.remedy} — with Retain, the old resource keeps the name, so a same-name replacement can never proceed.${typeChangeCollisionNote}`, "NAMED_REPLACEMENT_COLLISION");
45622
+ if (this.options.replace !== true) throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but the create-first attempt collided with the existing resource: ${createMsg}. ${nameOrigin.descriptor}, so the CloudFormation-style safe replacement order (create the new resource before deleting the old) cannot reuse the occupied name — CloudFormation refuses this shape with "cannot update a stack when a custom-named resource requires replacing". ${nameOrigin.remedy}, or re-run with \`cdkd deploy --replace\` to delete the old resource FIRST and recreate it under the same name (the resource is briefly unavailable while it is recreated).${typeChangeCollisionNote}`, "NAMED_REPLACEMENT_COLLISION");
45097
45623
  this.logger.info(` Create-first collided with the existing resource's name and --replace is set — deleting old ${logicalId} (${currentResource.physicalId}) first...`);
45098
45624
  deletedOldFirst = true;
45099
- createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateSecrets, updateReplacePolicy);
45625
+ createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, oldResourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateSecrets, updateReplacePolicy);
45100
45626
  }
45101
- if (!deletedOldFirst && createResult.physicalId === currentResource.physicalId) {
45627
+ if (equalIdIsSameResource && !deletedOldFirst && createResult.physicalId === currentResource.physicalId) {
45102
45628
  const idempotentNameOrigin = this.replacementNameOrigin(logicalId, currentResource.physicalId);
45103
45629
  if (updateReplacePolicy === "Retain") throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but its Create API is name-idempotent: the create-first attempt returned the existing resource (${currentResource.physicalId}) instead of creating a new one, and UpdateReplacePolicy: Retain pins that resource in place. ${idempotentNameOrigin.descriptor}. ${idempotentNameOrigin.remedy} — with Retain, the old resource keeps the name, so a same-name replacement can never proceed.`, "NAMED_REPLACEMENT_IDEMPOTENT_CREATE");
45104
45630
  if (this.options.replace !== true) throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but its Create API is name-idempotent: the create-first attempt returned the EXISTING resource (${currentResource.physicalId}) instead of creating a new one, so deleting the "old" resource would silently destroy the resource the deploy just reported as created. ${idempotentNameOrigin.descriptor}; ${idempotentNameOrigin.remedy}, or re-run with \`cdkd deploy --replace\` to delete the old resource FIRST and recreate it under the same name (the resource is briefly unavailable while it is recreated). Note: this branch is also reached when the old resource was deleted out-of-band and the physical id is name-derived — there the create was a genuine fresh create; \`--replace\` converges that case too.`, "NAMED_REPLACEMENT_IDEMPOTENT_CREATE");
45105
45631
  this.logger.info(` Create-first returned the existing resource (name-idempotent Create API) and --replace is set — deleting old ${logicalId} (${currentResource.physicalId}) first...`);
45106
45632
  deletedOldFirst = true;
45107
- createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateSecrets, updateReplacePolicy);
45633
+ createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, oldResourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateSecrets, updateReplacePolicy);
45108
45634
  }
45109
45635
  if (deletedOldFirst) {} else if (updateReplacePolicy === "Retain") {
45110
45636
  this.retainedOldOnReplacement.add(logicalId);
@@ -45114,7 +45640,7 @@ var DeployEngine = class DeployEngine {
45114
45640
  let cleanupFinalSnapshotId;
45115
45641
  let snapshotBlockedDelete = false;
45116
45642
  try {
45117
- cleanupFinalSnapshotId = await this.prepareFinalSnapshotForDelete(logicalId, resourceType, currentResource, updateReplacePolicy);
45643
+ cleanupFinalSnapshotId = await this.prepareFinalSnapshotForDelete(logicalId, oldResourceType, currentResource, updateReplacePolicy);
45118
45644
  } catch (snapshotError) {
45119
45645
  if (snapshotError instanceof CdkdError && snapshotError.code === "FINAL_SNAPSHOT_UNSUPPORTED") throw snapshotError;
45120
45646
  snapshotBlockedDelete = true;
@@ -45124,7 +45650,7 @@ var DeployEngine = class DeployEngine {
45124
45650
  let cleanupDeleteResult = void 0;
45125
45651
  let cleanupDeleteFailed = false;
45126
45652
  try {
45127
- cleanupDeleteResult = await oldDeleteProvider.delete(logicalId, currentResource.physicalId, resourceType, currentResource.properties, {
45653
+ cleanupDeleteResult = await oldDeleteProvider.delete(logicalId, currentResource.physicalId, oldResourceType, currentResource.properties, {
45128
45654
  expectedRegion: this.stackRegion,
45129
45655
  forceDataDelete: this.options.forceStatefulRecreation === true,
45130
45656
  ...cleanupFinalSnapshotId !== void 0 && { finalSnapshotIdentifier: cleanupFinalSnapshotId }
@@ -45980,5 +46506,5 @@ var DeployEngine = class DeployEngine {
45980
46506
  };
45981
46507
 
45982
46508
  //#endregion
45983
- export { secretBearing as $, SynthesisError as $i, recordMaskOnlyValue as $n, AssetManifestLoader as $r, s3BucketArn as $t, extractDeploymentEventError as A, getAwsClients as Ai, refuseMalformedState as An, BOOTSTRAP_MARKER_PREFIX as Ar, isImpossibleEmptyStoredAttribute as At, toCfnInteger as B, IntrinsicResolutionRefusalError as Bi, STATE_SOURCED_BASELINE_RULES as Bn, buildDockerImage as Br, coerceCfnBoolean as Bt, buildFinalSnapshotIdentifier as C, canonicalizeRegion as Ci, producerRecordKey as Cn, stringifyValue as Cr, isTerminationProtectionPropagationError as Ct, refusesFinalSnapshot as D, clearBucketRegionCache as Di, refuseMalformedOutputsForDestroy as Dn, loadPublishableAssetManifest as Dr, cfnRefValueFromPhysicalId as Dt, isFinalSnapshotError as E, processStackMessages as Ei, refuseMalformedOutputs as En, createAssetRedirectResolver as Er, carriesFetchableDynamicReference as Et, isStatefulRecreateTargetSync as F, ConfigError as Fi, findSilentDropProperties as Fn, parseBootstrapMarker as Fr, isSettledInstanceState as Ft, gray as G, NestedStackChildDirectDestroyError as Gi, createSecretMasker as Gn, formatDockerLoginError as Gr, readConfigString as Gt, formatResourceLine as H, LocalStartServiceError as Hi, STATE_SOURCED_READBACK_RULES as Hn, describeDockerExecFailure as Hr, configBooleanRefusal as Ht, renderStatefulReason as I, CrossAccountSecretRefusalError as Ii, describeTypeWithThrottleRetry as In, readBootstrapMarkerBody as Ir, WAFv2WebACLProvider as It, yellow as J, ResourceTimeoutError as Ji, identityKeyFor as Jn, redactDockerArgvValues as Jr, requireConfigObject as Jt, green as K, PartialFailureError as Ki, dynamicReferenceTokens as Kn, getDockerCmd as Kr, replayWarn as Kt, WARM_THROUGHPUT_MEMBERS as L, DependencyError as Li, withRetry as Ln, validateAssetBucketName as Lr, normalizeAwsTagsToCfn as Lt, planOrphanAdoption as M, setAwsClients as Mi, repairMalformedResourcePropertiesForReadOnly as Mn, ensureAssetStorage as Mr, isUnboundTemplateParameter as Mt, MULTI_REGION_RECREATE_BLOCKED_TYPES as N, AssetError as Ni, repairMalformedResourcesForReadOnly as Nn, getBootstrapMarkerKey as Nr, parameterTypeMayLoseSecretIdentity as Nt, unsupportedFinalSnapshotError as O, resolveBucketRegion as Oi, refuseMalformedResourcePropertiesForOrphan as On, rewriteTemplateAssetReferences as Or, coerceParameterTypedValue as Ot, isStatefulRecreateTargetForReplace as P, CdkdError as Pi, findActionableSilentDrops as Pn, isCrossRegionRedirect as Pr, refStateLookupFromResource as Pt, isExportAliasCollision as Q, StateError as Qi, maskSecretsInText as Qn, stripControlChars as Qr, withSharedDrainBudget as Qt, coerceWarmThroughput as R, DeployCancelledError as Ri, DagBuilder as Rn, validateContainerRepoName as Rr, resolveExplicitPhysicalId as Rt, PRE_DELETE_SNAPSHOT_TYPES as S, PARTITION_TABLE as Si, malformedStateRefusalMessage as Sn, AssetPublisher as Sr, disableInstanceApiTermination as St, createPreDeleteFinalSnapshot as T, AssemblyReader as Ti, refuseMalformedNestedChildOutputs as Tn, buildAssetRedirectMap as Tr, carriesDynamicReference as Tt, bold as U, LockError as Ui, TEMPLATE_SOURCED_RULES as Un, describeDockerFailure as Ur, configIntegerRefusal as Ut, toFiniteNumber as V, LocalInvokeBuildError as Vi, STATE_SOURCED_CROSS_GENERATION_RULES as Vn, describeDockerCapturedOutput as Vr, coerceCfnInteger as Vt, cyan as W, MalformedProducerRecordRefusalError as Wi, carriesSecretMask as Wn, dockerSpawnEnvWithSensitive as Wr, configStringRefusal as Wt, collectPublishedOutputNames as X, StackHasActiveImportsError as Xi, markSameGenerationBag as Xn, runDockerStreaming as Xr, classifyReplaySecretRegion as Xt, collectDeclaredOutputNames as Y, ResourceUpdateNotSupportedError as Yi, isSingleDynamicReferenceToken as Yn, runDockerForeground as Yr, requireConfigString as Yt, exportAliasCollisionScrubWarning as Z, StackTerminationProtectionError as Zi, maskSecretsInError as Zn, escapeRegExp$1 as Zr, producerRegionsFromState as Zt, isSecretBearingReferenceString as _, findLargeInlineResources as _i, malformedOutputsRefusalMessage as _n, importableOutputs as _r, isWaitAbandonedError as _t, DeploymentEventsStore as a, isRetryableTransientError as aa, resolveApp as ai, AWS_CREDENTIAL_ENV_KEYS as an, rebuildClientForBucketRegion as ar, clearOnUpdateRemoval as at, bindingSkippedOutputs as b, decodeListingKey as bi, malformedResourcePropertiesWarning as bn, orphansCarriedFrom as br, deleteIndeterminateGuards as bt, replayFailedOperations as c, markNonRetryable as ca, resolveSkipPrefix as ci, envCredentialsAreAssumedRole as cn, buildLockContentionMessage as cr, createMaskedRetryLogger as ct, updatePartialReason as d, describeAwsFailure as da, resolveUseCdkBootstrapAssets as di, STATE_RESOURCES_MALFORMED as dn, shellQuote as dr, beginCommandInterruptScope as dt, formatError as ea, getDockerImageBySourceHash as ei, s3BucketDomainName as en, recoverMaskedOutput as er, secretBearingStateKeyWarning as et, withResourceDeadline as f, safeStringify$1 as fa, stateBucketExistenceConfirmed as fi, hasReadableOutputs as fn, CUSTOM_RESOURCE_RESPONSE_PREFIX as fr, endCommandInterruptScope as ft, bagHoldsSecretExpression as g, MIGRATE_TMP_PREFIX as gi, malformedLocalOutputsWarning as gn, importableOutputKeys as gr, CloudControlProvider as gt, computeImplicitDeleteEdges as h, CFN_TEMPLATE_URL_LIMIT as hi, malformedExportSourceWarning as hn, hasReadableExportSet as hr, startInterruptWatch as ht, DeploymentEventsReader as i, isMarkedNonRetryable as ia, getLegacyStateBucketName as ii, applyRoleArnIfSet as in, S3StateBackend as ir, collectInlinePolicyNamesManagedBySiblings as it, makeSiblingClaimReader as j, resetAwsClients as ji, repairMalformedOutputsForReadOnly as jn, assertAssetBucketRegion as jr, isNamelessDynamicReferenceError as jt, makeCanonicalizePropertiesFn as k, AwsClients as ki, refuseMalformedResourcesForDestroy as kn, AssetModeResolver as kr, getAccountInfo as kt, replayRollback as l, markRedactedCause as la, resolveStateBucketWithDefault as li, DiffCalculator as ln, forceQuitRecoveryClause as lr, maskDeep as lt, IMPLICIT_DELETE_DEPENDENCIES as m, CFN_TEMPLATE_BODY_LIMIT as mi, malformedExportNamesWarning as mn, exportNamesCarriedFrom as mr, isInterruptedWaitError as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, normalizeAwsError as na, synthesisStatusMessage as ni, s3BucketRegionalDomainName as nn, scrubResourceRecord as nr, getCurrentResourceSecrets as nt, planFailedOps as o, isThrottlingError as oa, resolveAutoAssetStorage as oi, applyCallerIdentityCredentials as on, UNREPRODUCIBLE_LOCK_CLAUSE as or, ProviderRegistry as ot, maskingRetryLogger as p, __exportAll as pa, warnDeprecatedNoPrefixCliFlag as pi, hasReadableResources as pn, DEFAULT_STATE_PREFIX as pr, interruptWatchListenerCount as pt, red as q, ProvisioningError as qi, errorCauseChain as qn, partitionSensitiveEnv as qr, requireConfigArray as qt, DeployEngine as r, withErrorHandling as ra, getDefaultStateBucketName as ri, s3BucketWebsiteUrl as rn, LockManager as rr, IAMRoleProvider as rt, planRollback as s, isTransientServerError as sa, resolveCaptureObservedState as si, callerEnvCredentials as sn, buildForceUnlockCommand as sr, wouldReturnToSdkProvider as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, isCdkdError as ta, Synthesizer as ti, s3BucketDualStackDomainName as tn, redactSecretsForState as tr, secretSafeKeyDisplay as tt, updatePartialMessage as u, retryClassificationText as ua, resolveStateBucketWithDefaultAndSource as ui, INTRINSIC_KEYS as un, formatLockExpiry as ur, maskerOrIdentity as ut, keptWholeReasonText as v, uploadCfnTemplate as vi, malformedOutputsWarning as vn, isReadableBag as vr, slowCcOperationTimeoutMs as vt, ccRoutedFinalSnapshotError as w, derivePartitionAndUrlSuffix as wi, refuseDivergentRecordRegionForDestroy as wn, WorkGraph as wr, IntrinsicFunctionResolver as wt, ATOMIC_FINAL_SNAPSHOT_TYPES as x, expectedOwnerParam as xi, malformedResourcesWarning as xn, shouldRetainResource as xr, deleteSkipReason as xt, mergeNoChangeOutputs as y, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as yi, malformedRenderedContainersWarning as yn, orphansAfterRollback as yr, UNSPECIFIED_SKIP_REASON as yt, isWarmThroughputDecrease as z, DynamicReferenceRegionAmbiguousError as zi, TemplateParser as zn, buildDenyExternalAccessPolicy as zr, assertRegionMatch as zt };
45984
- //# sourceMappingURL=deploy-engine-CfOsVRQB.js.map
46509
+ export { isExportAliasCollision as $, StateError as $i, maskSecretsInText as $n, stripControlChars as $r, withSharedDrainBudget as $t, makeCanonicalizePropertiesFn as A, AwsClients as Ai, refuseMalformedResourcesForDestroy as An, AssetModeResolver as Ar, getAccountInfo as At, isWarmThroughputDecrease as B, DynamicReferenceRegionAmbiguousError as Bi, TemplateParser as Bn, buildDenyExternalAccessPolicy as Br, assertRegionMatch as Bt, PRE_DELETE_SNAPSHOT_TYPES as C, PARTITION_TABLE as Ci, malformedStateRefusalMessage as Cn, AssetPublisher as Cr, disableInstanceApiTermination as Ct, isFinalSnapshotError as D, processStackMessages as Di, refuseMalformedOutputs as Dn, createAssetRedirectResolver as Dr, carriesFetchableDynamicReference as Dt, createPreDeleteFinalSnapshot as E, AssemblyReader as Ei, refuseMalformedNestedChildOutputs as En, buildAssetRedirectMap as Er, carriesDynamicReference as Et, isStatefulRecreateTargetForReplace as F, CdkdError as Fi, findActionableSilentDrops as Fn, isCrossRegionRedirect as Fr, refStateLookupFromResource as Ft, cyan as G, MalformedProducerRecordRefusalError as Gi, carriesSecretMask as Gn, dockerSpawnEnvWithSensitive as Gr, configStringRefusal as Gt, toFiniteNumber as H, LocalInvokeBuildError as Hi, STATE_SOURCED_CROSS_GENERATION_RULES as Hn, describeDockerCapturedOutput as Hr, coerceCfnInteger as Ht, isStatefulRecreateTargetSync as I, ConfigError as Ii, findSilentDropProperties as In, parseBootstrapMarker as Ir, isSettledInstanceState as It, red as J, ProvisioningError as Ji, errorCauseChain as Jn, partitionSensitiveEnv as Jr, requireConfigArray as Jt, gray as K, NestedStackChildDirectDestroyError as Ki, createSecretMasker as Kn, formatDockerLoginError as Kr, readConfigString as Kt, renderStatefulReason as L, CrossAccountSecretRefusalError as Li, describeTypeWithThrottleRetry as Ln, readBootstrapMarkerBody as Lr, WAFv2WebACLProvider as Lt, makeSiblingClaimReader as M, resetAwsClients as Mi, repairMalformedOutputsForReadOnly as Mn, assertAssetBucketRegion as Mr, isNamelessDynamicReferenceError as Mt, planOrphanAdoption as N, setAwsClients as Ni, repairMalformedResourcePropertiesForReadOnly as Nn, ensureAssetStorage as Nr, isUnboundTemplateParameter as Nt, refusesFinalSnapshot as O, clearBucketRegionCache as Oi, refuseMalformedOutputsForDestroy as On, loadPublishableAssetManifest as Or, cfnRefValueFromPhysicalId as Ot, MULTI_REGION_RECREATE_BLOCKED_TYPES as P, AssetError as Pi, repairMalformedResourcesForReadOnly as Pn, getBootstrapMarkerKey as Pr, parameterTypeMayLoseSecretIdentity as Pt, exportAliasCollisionScrubWarning as Q, StackTerminationProtectionError as Qi, maskSecretsInError as Qn, escapeRegExp$1 as Qr, producerRegionsFromState as Qt, WARM_THROUGHPUT_MEMBERS as R, DependencyError as Ri, withRetry as Rn, validateAssetBucketName as Rr, normalizeAwsTagsToCfn as Rt, ATOMIC_FINAL_SNAPSHOT_TYPES as S, expectedOwnerParam as Si, malformedResourcesWarning as Sn, shouldRetainResource as Sr, deleteSkipReason as St, ccRoutedFinalSnapshotError as T, derivePartitionAndUrlSuffix as Ti, refuseDivergentRecordRegionForDestroy as Tn, WorkGraph as Tr, IntrinsicFunctionResolver as Tt, formatResourceLine as U, LocalStartServiceError as Ui, STATE_SOURCED_READBACK_RULES as Un, describeDockerExecFailure as Ur, configBooleanRefusal as Ut, toCfnInteger as V, IntrinsicResolutionRefusalError as Vi, STATE_SOURCED_BASELINE_RULES as Vn, buildDockerImage as Vr, coerceCfnBoolean as Vt, bold as W, LockError as Wi, TEMPLATE_SOURCED_RULES as Wn, describeDockerFailure as Wr, configIntegerRefusal as Wt, collectDeclaredOutputNames as X, ResourceUpdateNotSupportedError as Xi, isSingleDynamicReferenceToken as Xn, runDockerForeground as Xr, requireConfigString as Xt, yellow as Y, ResourceTimeoutError as Yi, identityKeyFor as Yn, redactDockerArgvValues as Yr, requireConfigObject as Yt, collectPublishedOutputNames as Z, StackHasActiveImportsError as Zi, markSameGenerationBag as Zn, runDockerStreaming as Zr, classifyReplaySecretRegion as Zt, bagHoldsSecretExpression as _, MIGRATE_TMP_PREFIX as _i, malformedLocalOutputsWarning as _n, importableOutputKeys as _r, CloudControlProvider as _t, DeploymentEventsStore as a, isMarkedNonRetryable as aa, getLegacyStateBucketName as ai, applyRoleArnIfSet as an, S3StateBackend as ar, collectInlinePolicyNamesManagedBySiblings as at, mergeNoChangeOutputs as b, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as bi, malformedRenderedContainersWarning as bn, orphansAfterRollback as br, UNSPECIFIED_SKIP_REASON as bt, replayFailedOperations as c, isTransientServerError as ca, resolveCaptureObservedState as ci, callerEnvCredentials as cn, buildForceUnlockCommand as cr, wouldReturnToSdkProvider as ct, updatePartialMessage as d, retryClassificationText as da, resolveStateBucketWithDefaultAndSource as di, INTRINSIC_KEYS as dn, formatLockExpiry as dr, maskerOrIdentity as dt, SynthesisError as ea, AssetManifestLoader as ei, s3BucketArn as en, recordMaskOnlyValue as er, secretBearing as et, updatePartialReason as f, describeAwsFailure as fa, resolveUseCdkBootstrapAssets as fi, STATE_RESOURCES_MALFORMED as fn, shellQuote as fr, beginCommandInterruptScope as ft, computeImplicitDeleteEdges as g, CFN_TEMPLATE_URL_LIMIT as gi, malformedExportSourceWarning as gn, hasReadableExportSet as gr, startInterruptWatch as gt, IMPLICIT_DELETE_DEPENDENCIES as h, CFN_TEMPLATE_BODY_LIMIT as hi, malformedExportNamesWarning as hn, exportNamesCarriedFrom as hr, isInterruptedWaitError as ht, DeploymentEventsReader as i, withErrorHandling as ia, getDefaultStateBucketName as ii, s3BucketWebsiteUrl as in, LockManager as ir, IAMRoleProvider as it, extractDeploymentEventError as j, getAwsClients as ji, refuseMalformedState as jn, BOOTSTRAP_MARKER_PREFIX as jr, isImpossibleEmptyStoredAttribute as jt, unsupportedFinalSnapshotError as k, resolveBucketRegion as ki, refuseMalformedResourcePropertiesForOrphan as kn, rewriteTemplateAssetReferences as kr, coerceParameterTypedValue as kt, replayRollback as l, markNonRetryable as la, resolveSkipPrefix as li, envCredentialsAreAssumedRole as ln, buildLockContentionMessage as lr, createMaskedRetryLogger as lt, maskingRetryLogger as m, __exportAll as ma, warnDeprecatedNoPrefixCliFlag as mi, hasReadableResources as mn, DEFAULT_STATE_PREFIX as mr, interruptWatchListenerCount as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, isCdkdError as na, Synthesizer as ni, s3BucketDualStackDomainName as nn, redactSecretsForState as nr, secretSafeKeyDisplay as nt, planFailedOps as o, isRetryableTransientError as oa, resolveApp as oi, AWS_CREDENTIAL_ENV_KEYS as on, rebuildClientForBucketRegion as or, clearOnUpdateRemoval as ot, withResourceDeadline as p, safeStringify$1 as pa, stateBucketExistenceConfirmed as pi, hasReadableOutputs as pn, CUSTOM_RESOURCE_RESPONSE_PREFIX as pr, endCommandInterruptScope as pt, green as q, PartialFailureError as qi, dynamicReferenceTokens as qn, getDockerCmd as qr, replayWarn as qt, DeployEngine as r, normalizeAwsError as ra, synthesisStatusMessage as ri, s3BucketRegionalDomainName as rn, scrubResourceRecord as rr, getCurrentResourceSecrets as rt, planRollback as s, isThrottlingError as sa, resolveAutoAssetStorage as si, applyCallerIdentityCredentials as sn, UNREPRODUCIBLE_LOCK_CLAUSE as sr, ProviderRegistry as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, formatError as ta, getDockerImageBySourceHash as ti, s3BucketDomainName as tn, recoverMaskedOutput as tr, secretBearingStateKeyWarning as tt, resolveReplacementOldType as u, markRedactedCause as ua, resolveStateBucketWithDefault as ui, DiffCalculator as un, forceQuitRecoveryClause as ur, maskDeep as ut, isSecretBearingReferenceString as v, findLargeInlineResources as vi, malformedOutputsRefusalMessage as vn, importableOutputs as vr, isWaitAbandonedError as vt, buildFinalSnapshotIdentifier as w, canonicalizeRegion as wi, producerRecordKey as wn, stringifyValue as wr, isTerminationProtectionPropagationError as wt, bindingSkippedOutputs as x, decodeListingKey as xi, malformedResourcePropertiesWarning as xn, orphansCarriedFrom as xr, deleteIndeterminateGuards as xt, keptWholeReasonText as y, uploadCfnTemplate as yi, malformedOutputsWarning as yn, isReadableBag as yr, slowCcOperationTimeoutMs as yt, coerceWarmThroughput as z, DeployCancelledError as zi, DagBuilder as zn, validateContainerRepoName as zr, resolveExplicitPhysicalId as zt };
46510
+ //# sourceMappingURL=deploy-engine-efiVJ-rX.js.map