/** * The retain flip — phase 2/3 of the two-deploy `forget` orchestration * (drift-remediation plan § 3.2). The flip template derives from the LIVE * template (`GetTemplate`, never synth): the ONLY mutations are * `DeletionPolicy: Retain` + `UpdateReplacePolicy: Retain` on the target * logical IDs plus the in-template `fjall:forget` metadata marker [G3] — the * AWS-side resume state that survives machine loss. * * Two proof guards, both required before the flip may apply: * (a) `verifyRetainFlipDiff` — a pure diff-guard that hard-fails if anything * beyond the allowed keys differs from the fetched template; * (b) `proveFlipMetadataOnly` — a `fjall-preflight-` review change set that * proves every reported change targets only * `{DeletionPolicy, UpdateReplacePolicy, Metadata}` on the target IDs. * Any property-level delta aborts to `repair` with the offending paths * attached (paths only, never values). Non-target changes are tolerated ONLY * as transitive dynamic reference re-evaluations caused by the targets — * CloudFormation conservatively re-reports every Ref/GetAtt dependent of a * metadata-touched resource (2026-07-12 live defect #4: the BC Account * stack's bucket, bucket policy and key alias re-evaluated off a flipped KMS * key, wrongly aborting the consented forget) — see * `classifyProofViolations`. * * [G10] `didn't contain changes` is NOT taken as "flip already live" — * `GetTemplate` re-verification (`readRetainFlipState`) is the sole authority * for the already-applied/resume-skip branch. * * FLIPLESS fast-path (2026-07-12 live defect #5): the UpdateStack here is * reserved for `policyPendingTargets` — targets whose live policies actually * need changing. Targets already carrying the spec's policy (the CDK default * for stateful types is Retain) are advanced by the orchestrators WITHOUT any * UpdateStack and WITHOUT a marker: CloudFormation re-resolves Ref/GetAtt * dependents of any modified dead resource at execution time, so even a * metadata-only Modify of a dead, referenced target rolls the update back — * and the policy the update would assert is already live, so the mutation * guarantees nothing the live template does not already prove. The same * incident's OUTBOUND direction (a policy-pending target whose OWN references * resolve against dead siblings — modifying it is structurally impossible) * is advanced flipless BY NECESSITY once the target itself is verified * physically deleted: the converge's removal of an already-deleted resource * is a physical no-op, so the Retain guarantee is not needed (the forget * orchestrator's classification; journal `fliplessByNecessity`). * * The flip `UpdateStack` OMITS the `Tags` parameter (CloudFormation preserves * existing stack tags, so no tag-driven resource updates can ride along), * echoes the stack's capabilities unioned with the proof's fixed set, and * echoes `UsePreviousValue: true` for every live stack parameter — omitting * `Parameters` would resolve the template DEFAULTS (e.g. `ImageTag` * back to "latest"), turning the metadata-only flip into a real resource * update. The proof change set echoes the same parameters for the same * reason. Immediately before the UpdateStack, the live template digest is * re-checked against the template the flip derived from — the proof change * set is review-only and never executed, so nothing else binds the apply to * the proven bytes. * * [G8] boundary audit (2026-07-08): the flip's mutations — * cloudformation:CreateChangeSet/DescribeChangeSet/DeleteChangeSet/UpdateStack * — target the app's OWN stack, which the deploy session policy's * CFN-mutation Deny exempts via its `stack/${appPascal}*` NotResource * (`deploySessionPolicy.ts`); template staging rides the inherit-admin allow * like the change-set probe's. A repair aimed at a foreign `--stack` under an * OIDC deploy session correctly AccessDenies — defence-in-depth, not a gap. * No session-policy change needed. * * Failure messages are pre-masked at construction. */ import { Capability, type Change } from "@aws-sdk/client-cloudformation"; import { type Result } from "@fjall/generator"; import type { AwsProvider } from "../../aws/AwsProvider.js"; /** The in-template resume marker [G3]. */ export declare const FORGET_MARKER_KEY = "fjall:forget"; /** The recreate orchestration's in-template resume marker (Phase 7) — a * distinct key so the two surgeries' resume/ownership scans never adopt each * other's markers. */ export declare const RECREATE_MARKER_KEY = "fjall:recreate"; export type FlipDeletionPolicy = "Retain" | "Snapshot" | "Delete"; /** * What the flip stamps: the marker key and the DeletionPolicy / * UpdateReplacePolicy value per target. The forget flip retains everything; * the recreate snapshot-flip stamps Snapshot where the resource type supports * it and Delete otherwise. Every read/derive/diff helper below takes the spec * so both surgeries share one proven flip machine; the default is the forget * spec, keeping every pre-Phase-7 call site byte-identical. */ export interface FlipSpec { markerKey: string; policyForTarget: (logicalId: string) => FlipDeletionPolicy; } export declare const FORGET_FLIP_SPEC: FlipSpec; /** * Proof and apply MUST request the same capability set: a proof that * hardcodes a superset the apply lacks means a transform-bearing stack proves * clean and then dies at UpdateStack with InsufficientCapabilities. Union of * the stack's recorded capabilities (invalid values filtered) with the fixed * proof set — one helper, both call sites. */ export declare function composeFlipCapabilities(recorded: readonly string[]): Capability[]; /** * Echo `UsePreviousValue: true` for every live stack parameter on both flip * CFN calls (mirrors `CloudFormationService.updateStackParameters`). Deployed * Compute stacks carry `ImageTag` CfnParameters whose live value is * a content hash but whose template default is "latest" — omitting * `Parameters` resolves the DEFAULTS, so the proof would report Image → * repo:latest → Replacement on the task definition and hard-fail every * attempt, and the apply would roll images back. Zero-parameter stacks omit * the property entirely. */ export declare function echoPreviousParameters(parameterKeys: readonly string[]): { Parameters: Array<{ ParameterKey: string; UsePreviousValue: true; }>; } | Record; export interface RetainFlipOptions { pollIntervalMs?: number; changeSetTimeoutMs?: number; updateTimeoutMs?: number; /** Injectable clock for timeout tests. */ now?: () => number; } export declare function computeTemplateDigest(templateBody: string): string; export interface RetainFlipState { /** Every target carries the spec's policy + marker. */ allFlipped: boolean; /** Targets no longer present in the live template (removal landed). */ absentTargets: string[]; /** Targets present but not (fully) flipped. */ unflippedTargets: string[]; /** * Targets present whose DeletionPolicy/UpdateReplacePolicy do NOT match the * spec — the ones an UpdateStack would actually have to change. Disjoint * from the flipless set (present, policies satisfied, marker absent = * `unflippedTargets` minus this list). The POLICIES are the safety property * the flip exists to guarantee; the marker is resume/ownership bookkeeping — * so this list, not `unflippedTargets`, gates the flip UpdateStack and the * [G2] partition's flip-live check (2026-07-12 live defect #5: * CloudFormation re-resolves Ref/GetAtt dependents of ANY modified dead * resource at execution time, so an UpdateStack that merely re-states an * already-live Retain policy to add a marker rolls back — and is * unnecessary, because the policy it would assert is already live). */ policyPendingTargets: string[]; /** Distinct opIds found in the targets' markers (the spec's key only). */ markerOpIds: string[]; } /** * [G3]/[G10] authority helper: read the flip state straight from the live * template. Shared by the flip-applied verify, resume re-derivation, and the * [G2] pre-flight's mid-surgery check. */ export declare function readRetainFlipState(liveTemplateBody: string, targets: readonly string[], stackName: string, spec?: FlipSpec): Result; /** * [G3] recovery scan: every logical ID in the live template whose * fjall:forget marker carries the given opId. Recovery is LIVE-TEMPLATE * scoped: markers embed only `{opId, at}` per-resource, so a marked resource * that was since stripped (converged away, manually edited) is invisible here * and the recovered set may be a PROPER SUBSET of the operation's original * target set. Callers that adopt a foreign operation from this scan MUST * prove completeness via hash identity — `computeRemediationOpId(stackName, * recovered)` reproducing the marker's opId is the only proof the full set * was recovered; adopting a subset mints a different opId whose surgery * meets the original markers as foreign and livelocks. */ export declare function findMarkedResourcesByOpId(liveTemplateBody: string, opId: string, stackName: string, markerKey?: string): Result; /** * CFN resource Type per logical ID, read from the live template — the * adoption seam records these on targets recovered from markers alone (no * journal, no drift finding to carry the type). Unreadable template or * missing/typeless resource simply omits the entry. */ export declare function readResourceTypes(liveTemplateBody: string, logicalIds: readonly string[], stackName: string): Record; export interface DeriveRetainFlipParams { liveTemplateBody: string; stackName: string; targets: readonly string[]; opId: string; /** ISO-8601 stamp recorded in the marker. */ at: string; /** Marker key + per-target policy — defaults to the forget flip. */ spec?: FlipSpec; } export interface RetainFlipDerivation { flipTemplateBody: string; liveTemplateDigest: string; /** True when every target already carried the policy + OUR opId's marker. */ alreadyApplied: boolean; } /** * Build the flip template from the live template: mutate ONLY * DeletionPolicy/UpdateReplacePolicy → the spec's per-target policy and stamp * the spec's marker on the target logical IDs. A target carrying a DIFFERENT * operation's marker fails closed — two overlapping surgeries on one resource * means the operator's model of the stack is wrong. */ export declare function deriveRetainFlipTemplate(params: DeriveRetainFlipParams): Result; /** * Pure diff-guard (§3.2 guard a): strip the allowed flip keys from both * templates and require byte-identical remainders. Any residual difference * hard-fails with the offending paths (paths only, never values). */ export declare function verifyRetainFlipDiff(liveTemplateBody: string, flipTemplateBody: string, targets: readonly string[], stackName: string, markerKey?: string): Result; export interface FlipProofResult { /** True when CFN reported the flip contains no changes at all [G10] — * the caller MUST re-verify via GetTemplate, never assume flip-live. */ noChanges: boolean; } /** * Classify the proof change set's reported changes; an empty return proves * the flip metadata-only. Targets may only carry Modify actions scoped to * `ALLOWED_FLIP_ATTRIBUTES` with no replacement. Non-targets are tolerated * ONLY via `isReferenceReEvaluationOf`, computed to a fixpoint because the * re-evaluation propagates transitively (2026-07-12 live defect #4: the BC * Account stack's bucket policy re-evaluated off the bucket's ARN, which * itself re-evaluated off the flipped KMS key's ARN). A logical ID reported * by several change entries is tolerated only if EVERY entry qualifies. */ export declare function classifyProofViolations(changes: readonly Change[], targets: readonly string[]): string[]; export interface ProveFlipMetadataOnlyParams { stackName: string; flipTemplateBody: string; targets: readonly string[]; /** Live parameter keys, echoed UsePreviousValue on the change set — see * `echoPreviousParameters`. */ parameterKeys: readonly string[]; abortSignal?: AbortSignal; } /** * Change-set proof (§3.2 guard b): create a review-only `fjall-preflight-` * change set for the flip template and prove every reported change targets * only {DeletionPolicy, UpdateReplacePolicy, Metadata} on the target IDs — * tolerating non-target changes only where `classifyProofViolations` proves * them transitive dynamic reference re-evaluations of the targets. * Nothing is ever executed; the change set is deleted in `finally`. */ export declare function proveFlipMetadataOnly(aws: AwsProvider, params: ProveFlipMetadataOnlyParams, options?: RetainFlipOptions): Promise>; export interface ApplyRetainFlipParams { stackName: string; flipTemplateBody: string; /** The stack's existing capabilities — unioned with the proof's fixed set * via `composeFlipCapabilities` so proof and apply request the same set. */ capabilities: readonly string[]; /** Live parameter keys, echoed UsePreviousValue on the UpdateStack — see * `echoPreviousParameters`. */ parameterKeys: readonly string[]; /** Digest of the live template the flip derived from — re-checked against * a fresh GetTemplate immediately before the UpdateStack (the proof change * set is never executed, so nothing else binds apply to proof). */ derivedFromTemplateDigest: string; /** Names the resume command in the mid-surgery abort message. */ opId: string; /** Failure-message label — the recreate arm's removal update reuses this * apply machine and labels itself honestly. Default "Retain-flip". */ operationLabel?: string; /** Mid-surgery digest-mismatch cure line — defaults to the forget arm's * `fjall drift resume ` teach. */ resumeHint?: string; abortSignal?: AbortSignal; } export type ApplyRetainFlipOutcome = { outcome: "updated"; } /** CFN reported "No updates are to be performed" — the caller re-verifies * the flip via GetTemplate [G10], never assumes it landed. */ | { outcome: "no-updates"; }; /** * Execute the flip via direct SDK UpdateStack — flip TemplateBody, Tags * OMITTED (CFN preserves existing stack tags), capabilities echoed — then * poll to a terminal status. A metadata-only update issues no call against * the dead TARGET itself, but CloudFormation re-resolves Ref/GetAtt * attributes of every modified resource for its dependents at execution time * — which fails for a dead target with live referrers (2026-07-12 live * defect #5: "Unable to retrieve Arn attribute"). Callers MUST therefore * reserve this UpdateStack for policy-pending targets and guard the * referrer topology first (the forget orchestrator's flipless fast-path + * referrer guard; the recreate arm's pre-flight removal derivation). */ export declare function applyRetainFlip(aws: AwsProvider, params: ApplyRetainFlipParams, options?: RetainFlipOptions): Promise>; /** * Inline body up to CFN's 51,200-byte limit; larger templates stage in the * CDK bootstrap assets bucket (the change-set probe's staging shape). Shared * with the recreate arm's removal update (`removalUpdate.ts`). */ export declare function resolveTemplateLocation(aws: AwsProvider, keySuffix: string, templateBody: string, abortSignal?: AbortSignal): Promise>; /** Best-effort cleanup of the staged template — logged, never thrown. */ export declare function deleteStagedTemplate(aws: AwsProvider, key: string): Promise;