/** * The surgical removal update — the recreate orchestration's middle phase * (drift-remediation plan § Phase 7): derive a removal template from the LIVE * template (`GetTemplate`, never synth) with the target resources deleted, so * CloudFormation deletes the physical resources under the snapshot-flip's * DeletionPolicy (Snapshot where supported) and frees the pinned names for * the converge deploy to re-create. * * Derivation is deliberately conservative: * - target resources are removed; each remaining resource's `DependsOn` * edges to a removed resource are pruned (a dangling DependsOn fails * validation); * - Outputs whose subtree references a removed resource (Ref / Fn::GetAtt / * Fn::Sub) are removed (a dangling output reference fails validation); * - a REMAINING RESOURCE that references a removed resource is resolved by * the co-removal closure: a referrer on the explicit provably-safe * allowlist (`CO_REMOVABLE_RESOURCE_TYPES` — the DatabaseFactory scaffold * glue that ALWAYS pairs a pinned-name database with a * SecretTargetAttachment referrer, so failing on every referrer would make * the flagship recreate impossible on scaffold topology) joins the removal * set, because CloudFormation re-resolves a remaining referrer's * Ref/GetAtt at execution time and leaving it behind dooms the update. * EVERY other referrer fails the derivation closed with the referencing * paths (paths only, never values) — the ceremony consented to the targets * alone, and `isStatefulResourceType` is a data-loss LABEL, not a * deletion-authorisation gate: types it does not list (nested-stack * containers with stateful children, EC2 instances with attached volumes, * Custom:: providers whose Delete handlers run arbitrary code) can still * destroy data or state. An allowlisted referrer * carrying a retaining DeletionPolicy also fails closed: CloudFormation * would DELETE_SKIPPED it, and the converge's re-create would collide with * the orphaned physical resource. Co-removals are reported to the caller, * not consent-bearing: no data is lost by construction, and the converge * re-creates whatever the synthesised template still declares. * * `proveRemovalTargetsOnly` mirrors the retain flip's review change set: a * `fjall-preflight-` change set must report ONLY `Remove` actions on the * target logical IDs before the apply may run. The apply itself reuses * `applyRetainFlip` (template-agnostic UpdateStack + TOCTOU digest re-check + * poll) under a "Recreate-removal" label. * * Failure messages are pre-masked at construction (all content is * identifier-only: logical IDs, paths, stack names). */ import { type Result } from "@fjall/generator"; import type { AwsProvider } from "../../aws/AwsProvider.js"; import { type RetainFlipOptions } from "./retainFlip.js"; /** * The logical IDs of RESOURCES that reference any target via Ref, Fn::GetAtt * or Fn::Sub (DependsOn excluded — it names an ordering edge, not a value * CloudFormation must resolve). The forget arm's defect-#5 referrer guard: * CloudFormation re-resolves a modified resource's Ref/GetAtt for its * dependents at execution time, which fails for a DEAD target — so a flip * UpdateStack on a policy-pending dead target with live referrers can never * succeed and must fail closed before any mutation. Reuses the removal * derivation's canonical reference traversal (single source of truth). * * Unlike the removal derivation (where targets vanish together, so * target-to-target references are moot), the flip MODIFIES targets in place — * a target referencing a modified dead sibling is re-evaluated exactly like * an outside referrer (the live incident proved even DEAD referrers fail the * re-resolution), so distinct targets referencing each other are reported. * * Scan covers the Resources section only. An Output referencing a modified * dead target is NOT flagged: whether CloudFormation re-evaluates Outputs at * execution time is unproven live, and Output-references-stateful-resource is * so common in CDK synth that guarding on it would block legitimate flips; a * missed referrer degrades to a clean rollback, never an unsafe mutation * (residual recorded in the flipless ADR, 2026-07-12). */ export declare function findTargetReferrers(liveTemplateBody: string, targets: readonly string[], stackName: string): Result; export interface DeadReferenceFromTarget { /** The policy-pending target whose own properties carry the reference. */ pendingTarget: string; /** The dead siblings it references (Ref / Fn::GetAtt / Fn::Sub). */ deadReferences: string[]; } /** * The policy-pending targets whose OWN properties reference another dead * target. The defect-#5 guard's outbound direction: the second live incident * (2026-07-12, KMS alias flip) proved CloudFormation also resolves a MODIFIED * resource's own outbound Ref/GetAtt at execution time — even when the * referenced resource is not itself modified — so a flip UpdateStack on a * pending target referencing ANY dead sibling rolls back exactly like the * inbound referrer case. `findTargetReferrers` cannot see this shape: it * scans for references TO the pending set, and the doomed reference points * FROM the pending target at a sibling outside that set. * * `deadTargets` is the caller's known-deleted set (for the forget arm: the * op's targets minus any retained-by-redeclaration, which are proven * physically present and safe to reference). Self-references are excluded to * mirror the intra-set precedent, though CloudFormation rejects them anyway. */ export declare function findDeadReferencesFromTargets(liveTemplateBody: string, pendingTargets: readonly string[], deadTargets: readonly string[], stackName: string): Result; /** An Output the derivation stripped that carries an `Export` block — the * pre-flight must prove no other stack imports it before any mutation. */ export interface StrippedExport { outputName: string; /** The literal export name; undefined when `Export.Name` is not a plain * string (unresolvable locally — the pre-flight fails closed on it). */ exportName?: string; } /** * The resource types the co-removal closure may pull into the removal set * unconditionally. Membership requires ALL of: the type holds no data, * deleting it runs no side effects beyond detachment (no Custom:: Delete * handlers, no nested children), and the converge re-creates it verbatim from * synth. One further type is admitted by predicate, not membership: * `AWS::IAM::Policy` via `iamPolicyCoRemovalBlocker` (safe only when it * attaches exclusively to removal-set roles). Everything else fails the * derivation closed — the ceremony consented to the TARGETS only, and * `isStatefulResourceType` is a data-loss label, not a * deletion-authorisation gate (it does not list nested-stack containers, * EC2 instances or Custom:: providers, all of which can destroy data or run * delete-time code). */ export declare const CO_REMOVABLE_RESOURCE_TYPES: readonly ["AWS::SecretsManager::SecretTargetAttachment", "AWS::SecretsManager::RotationSchedule"]; /** An allowlisted glue resource the derivation pulled into the removal * because it references a removal-set member — CloudFormation re-resolves a * remaining referrer's Ref/GetAtt/Fn::Sub at execution time, so a referrer * left behind dooms the removal update. Allowlist-only by construction * (`CO_REMOVABLE_RESOURCE_TYPES`): every other referrer fails the derivation * closed instead. */ export interface CoRemovedResource { logicalId: string; resourceType: string; /** Where it references the removal set (identifier-only paths). */ referencePaths: string[]; } export interface RemovalDerivation { removalTemplateBody: string; liveTemplateDigest: string; /** Targets already absent from the live template (removal landed). */ alreadyAbsent: string[]; /** Stripped Outputs carrying an Export — cross-stack consumers of these * make the removal update fail mid-deploy (CFN refuses to drop an export * while imports exist); `runRecreatePreFlight` checks them via ListImports. */ strippedExports: StrippedExport[]; /** Allowlisted glue referrers co-removed with the targets * (inbound-reference fixpoint closure over `CO_REMOVABLE_RESOURCE_TYPES`). * Reported to the operator, never consent-bearing — no data loss by * construction. */ coRemoved: CoRemovedResource[]; } /** * Derive the removal template: live template minus the target resources and * the co-removal closure of their allowlisted glue referrers, with DependsOn * edges pruned and removal-referencing Outputs dropped. Any remaining * resource referencing the removal set that is type-less, stateful, off the * `CO_REMOVABLE_RESOURCE_TYPES` allowlist, or carrying a retaining * DeletionPolicy fails the derivation closed with the paths. */ export declare function deriveRemovalTemplate(liveTemplateBody: string, targets: readonly string[], stackName: string): Result; /** * The subset of `logicalIds` still present in the template's Resources — * the surgery's post-removal absence check over targets + co-removed alike * (`readRetainFlipState` is flip-target-oriented; presence is all that * matters here). */ export declare function findResourcesPresent(templateBody: string, logicalIds: readonly string[], stackName: string): Result; export interface RemovalDeletionFailure { logicalId: string; /** DELETE_FAILED, or DELETE_SKIPPED (a retaining-policy escape). */ status: string; /** Masked at construction — a status reason can embed handler output. */ statusReason?: string; } /** * GetTemplate absence alone cannot prove the physical deletions succeeded: * CloudFormation deletes removed resources during the update-CLEANUP phase, * and a DELETE_FAILED there leaves the stack UPDATE_COMPLETE with the target * gone from the template but orphaned alive — still holding its pinned name, * so the converge's re-create would collide. DELETE_SKIPPED is the same * orphan via a retaining DeletionPolicy: the derivation refuses retain-policy * co-removals, so a skip here means the template or policy changed mid-run — * fail closed regardless. Scan the just-settled operation's events on the * targets. Only meaningful immediately after this run's removal update (the * scan reads the LAST operation's events). */ export declare function findRemovalDeletionFailures(aws: AwsProvider, stackName: string, targets: readonly string[], abortSignal?: AbortSignal): Promise>; export interface RemovalProofResult { /** CFN reported the removal contains no changes — the caller MUST re-verify * target absence via GetTemplate, never assume the removal landed. */ noChanges: boolean; } export interface ProveRemovalTargetsOnlyParams { stackName: string; removalTemplateBody: string; targets: readonly string[]; /** Live parameter keys, echoed UsePreviousValue (see retainFlip). */ parameterKeys: readonly string[]; abortSignal?: AbortSignal; } /** * Review-only `fjall-preflight-` change set proving every reported resource * change is a `Remove` on a target logical ID. Nothing is executed; the * change set is deleted in `finally`. */ export declare function proveRemovalTargetsOnly(aws: AwsProvider, params: ProveRemovalTargetsOnlyParams, options?: RetainFlipOptions): Promise>;