/** * The `recreate` orchestrator — the snapshot-flip variant of the forget * surgery (drift-remediation plan § Phase 7), for the pinned-name-replacement * class: CloudFormation refuses to replace a custom-named resource in place * ("cannot update a stack when a custom-named resource requires replacing"), * so fjall performs the replacement surgically inside the SAME deploy run: * * captured → flip-verified (snapshot flip) → flip-applied → * removal-applied (surgical UpdateStack deletes the physical resource, * snapshotting per policy) → (the deploy continues and re-CREATEs the * resource under its pinned name) → done (post-deploy sweep) * * Unlike forget, the removal phase IS here: the synthesised template still * declares the resource, so the converge cannot remove it — a surgical * live-template-minus-targets update (`removalUpdate.ts`) deletes it first, * freeing the pinned name. Closure then flows through the SAME machinery as * forget (`completeForgetAfterConverge` → `resolveConvergedTargets`, marker * key derived from the journal's verb): a re-created target is * present-unmarked and physically probed live. Absent targets at sweep * closure were removed instead of re-created (the config no longer declares * them) — correct to close, the deletion was consented. * * Consent is the CALLER's job (the destruction ceremony runs before 3f * invokes this). Resumability rides the deploy path, not `drift resume`: at * any phase before removal-applied the pinned-name finding still fires, the * ceremony re-consents, and this surgery resumes idempotently; after * removal-applied the plan shows a plain CREATE (no ticket) and the sweep * closes the journal. `drift resume`/`drift repair` fail closed on recreate * journals (verb guard in `forgetResource.ts`) teaching the deploy re-run. * * Cross-incarnation edge: a NEW pinned-name finding on the same stack + * logical-ID set reuses the opId (derivation excludes the verb and has no * incarnation component). A resume that finds the prior incarnation at * removal-applied first attempts closure; a closed journal then archives and * restarts fresh under `doneJournalPolicy: "archive-and-restart"` (the 3f * caller's policy), so the new consent gets a new surgery. * * The `fjall:recreate` marker key is deliberately distinct from * `fjall:forget`: the two surgeries' resume/ownership scans never adopt each * other's markers. Overlapping forget + recreate operations on one resource * are an operator error, guarded at the opId collision point by the journal * verb checks. * * Failure messages are pre-masked at construction. */ import { type Result } from "@fjall/generator"; import type { AwsProvider } from "../../aws/AwsProvider.js"; import type { DriftFinding } from "../../types/drift.js"; import { type RemediationJournalRecord, type RemediationPhase, type RemediationTarget } from "../../types/remediation.js"; import { type ForgetOrchestrationOptions } from "./forgetResource.js"; import { type FlipDeletionPolicy } from "./retainFlip.js"; /** Options shape shared with the forget orchestrator (coupled machinery). */ export type RecreateOrchestrationOptions = ForgetOrchestrationOptions; /** * CFN resource types that support `DeletionPolicy: Snapshot` * (CloudFormation UserGuide § aws-attribute-deletionpolicy). Everything else * flips to `Delete` — the data loss was consented at the ceremony; the * snapshot is best-effort mitigation where the platform offers it. */ export declare const SNAPSHOT_CAPABLE_RESOURCE_TYPES: ReadonlySet; export declare function snapshotPolicyForType(resourceType: string): Extract; export interface RecreateSurgeryParams { stackName: string; targets: readonly RemediationTarget[]; /** The deploy target that owns the stack — names the resume command. */ appName?: string; /** Evidence persisted in the forensics bundle. */ findings?: readonly DriftFinding[]; /** Audit trail: the exact physical names consented at the ceremony. */ consentedPhysicalNames?: readonly string[]; /** Mirror of the forget arm's policy — the 3f deploy caller passes * "archive-and-restart" so a done journal from a prior incarnation of the * same stack + logical-ID set never false-reports completion. */ doneJournalPolicy?: "report-completed" | "archive-and-restart"; /** * [G7] Logical IDs the CURRENT deploy plan replaces under a pinned name. * A removal-applied resume whose closure could NOT verify a target * (converge-pending) must FAIL CLOSED when that target overlaps this set — * a converge-pending success would fall through to the native pinned-name * replacement CloudFormation refuses, burning the run. The 3f caller passes * its consented rows' logical IDs (by construction the plan replaces every * one of them); callers with no planned replacement may omit. */ plannedReplacementLogicalIds?: readonly string[]; onProgress?: (message: string) => void; abortSignal?: AbortSignal; } export interface RecreateSurgeryReport { opId: string; /** Journal phase after this pass. */ phase: RemediationPhase; /** True until the deploy re-creates the targets and the post-deploy sweep * closes the journal — the caller MUST continue the deploy on true. */ convergePending: boolean; forensicsPath?: string; /** The DeletionPolicy stamped per target logical ID. */ snapshotPolicies: Record; journal: RemediationJournalRecord; } /** * The orchestrated recreate: forensics capture (abort on failure — no * capture, no mutate), snapshot-flip derivation + both proofs, flip apply, * surgical removal (proof + apply + GetTemplate absence verification), then * removal-applied. The report is convergePending — the SAME deploy run * continues past the surgery and re-creates the resource under its pinned * name; the post-deploy sweep closes the journal. Idempotent and resumable: * safe to re-run at any phase (resumption rides the deploy path — the * pinned-name finding re-fires and re-consents until the removal lands). */ export declare function runRecreateSurgery(aws: AwsProvider, params: RecreateSurgeryParams, options?: RecreateOrchestrationOptions): Promise>;