/** * Destroy mirror for delegated child domains (D8; dev-substrate design :121): * the child's NS record is deleted from the PARENT zone BEFORE the CFN * destroy. Destroying first releases the child zone's name-server set back to * the Route53 pool while the parent still delegates to it — a * subdomain-takeover window. Delete-first trades a harmless orphaned-child- * zone leak (if the subsequent destroy fails) for eliminating that window. * * Ownership detection: when the NS record is CFN-owned by the child stack's * `CrossAccountZoneDelegationRecord` custom resource, the CFN destroy deletes * it in-stack — the manual delete is SKIPPED (deleting it out-of-band first * would fail the custom resource's own Delete). Only an out-of-stack record * (written by an earlier orchestration or by hand) is deleted here. * * Consent: the deletion is destructive-adjacent and is NEVER a silent side * effect — it is named as a row on the destruction ticket for the stack * destroy and evaluated through the SAME `evaluateDestructionConsents` + * digest TOCTOU path as every other ceremony (destruction-ceremony ADR: one * primitive, five renderings). Consent withheld ⇒ nothing is deleted and the * destroy does not start (exit-4 semantics at the surface). */ import { Route53Client } from "@aws-sdk/client-route-53"; import { type Result } from "@fjall/generator"; import type { DeployCallbacks } from "../../types/callbacks.js"; import type { DestructionConsent, DestructionConsentGate, DestructionConsentReport, DestructionOutcome, DestructionTicket } from "../../types/destruction.js"; import type { AwsProvider } from "../../aws/AwsProvider.js"; import { DomainDelegationError } from "./types.js"; /** * The custom-resource type CDK's `CrossAccountZoneDelegationRecord` emits — * its presence in the child stack's deployed template proves the NS record is * CFN-owned and will be deleted by the stack destroy itself. */ export declare const CROSS_ACCOUNT_DELEGATION_RESOURCE_TYPE = "Custom::CrossAccountZoneDelegation"; /** * Inspect a deployed CloudFormation template (JSON — the only form CDK * emits) for the delegation custom resource. An unreadable template is a * failure, not a guess: mis-detecting ownership either leaves a dangling * delegation (takeover window) or breaks the custom resource's own Delete. */ export declare function templateOwnsDelegationRecord(templateBody: string): Result; export type NsDeleteOutcome = { kind: "deleted"; recordValues: readonly string[]; } /** The record was already gone — NotFound on delete is success. */ | { kind: "record-absent"; } /** The parent zone itself is gone — nothing can dangle. */ | { kind: "parent-zone-absent"; }; export interface DeleteChildNsParams { /** Base provider; the delegation role is assumed from it by literal ARN. */ aws: AwsProvider; /** Parent DelegationRole ARN — always a literal (VD2-a), never an import. */ delegationRoleArn: string; parentZoneName: string; childZoneName: string; abortSignal?: AbortSignal; callbacks?: DeployCallbacks; /** Test seam: replaces the assume-role + client construction. */ route53ClientFactory?: (roleArn: string) => Promise, Error>>; } /** * Delete the child zone's NS record from the parent zone via the parent * DelegationRole (assumed by literal ARN). Idempotent: an absent record or an * absent parent zone is success, so the mirror self-heals across retries. * Every SDK call composes the caller's abort signal with the per-call * timeout (robustness standards). */ export declare function deleteChildNsFromParent(params: DeleteChildNsParams): Promise>; /** How the NS delegation was handled ahead of the stack destroy. */ export type DelegationDestroyNsOutcome = NsDeleteOutcome /** In-stack custom resource — the CFN destroy deletes it itself. */ | { kind: "cfn-owned"; } /** * Case-3 externally-owned parent: fjall cannot delete the record; the * operator consented `forget` (the record survives until removed by hand). */ | { kind: "external-forget"; }; export type DelegatedDomainDestroyOutcome = { kind: "destroyed"; nsDelegation: DelegationDestroyNsOutcome; } /** Consent withheld — nothing was deleted, the destroy did not start. */ | { kind: "withheld"; outcome: DestructionOutcome; ticket: DestructionTicket; report?: DestructionConsentReport; reasons: string[]; }; export interface DelegatedDomainDestroyParams { /** The delegated child zone being destroyed, e.g. `dev.example.com`. */ childZoneName: string; /** The parent zone holding the NS delegation, e.g. `example.com`. */ parentZoneName: string; /** * Parent DelegationRole ARN (literal, from the managed-domain binding). * Absent ⇒ Case-3 externally-owned parent: the record cannot be deleted by * fjall, and proceeding requires an explicit `forget` consent. */ delegationRoleArn?: string; /** The child domain stack's name (ticket addressing + template fetch). */ stackName: string; /** * Fetch the child stack's deployed template for ownership detection. * `undefined` data ⇒ the stack no longer exists (a previous destroy got * further than this step) — the mirror still runs, because the NS record * may be the surviving half. */ getTemplate: (abortSignal?: AbortSignal) => Promise>; /** Executes the CFN destroy — invoked ONLY after the mirror step. */ destroyStack: (callbacks: DeployCallbacks) => Promise>; aws: AwsProvider; callbacks: DeployCallbacks; /** Flag-supplied consents (`--remediate =`). */ consents?: readonly DestructionConsent[]; /** Digest the consents were minted against (TOCTOU binding). */ consentDigest?: string; /** Interactive ceremony resolver — consulted only when no flags came. */ consentGate?: DestructionConsentGate; abortSignal?: AbortSignal; /** Test seam forwarded to `deleteChildNsFromParent`. */ route53ClientFactory?: DeleteChildNsParams["route53ClientFactory"]; } /** The exact consent string for the NS row: `/NS`. */ export declare function nsDelegationPhysicalName(childZoneName: string): string; /** * Orchestrate the destroy of a delegated child domain stack: ownership * detection → destruction-consent ceremony (the NS deletion named on the * ticket) → NS delete (delete-first, idempotent) → CFN destroy. The * ordering is structural — `destroyStack` is only reachable after the mirror * step resolved — so no caller can destroy first by accident. */ export declare function runDelegatedDomainDestroy(params: DelegatedDomainDestroyParams): Promise>;