import { Route53Client } from "@aws-sdk/client-route-53"; import { type Result } from "@fjall/generator"; import type { AwsProvider } from "../../aws/AwsProvider.js"; import type { CloudFormationService } from "../../services/infrastructure/CloudFormationService.js"; import type { DeployCallbacks } from "../../types/callbacks.js"; import type { DestructionConsent, DestructionConsentGate, DestructionConsentReport, DestructionOutcome, DestructionTicket } from "../../types/destruction.js"; import { type Route53RecordCollision } from "../drift/route53RecordPreflight.js"; import type { DomainConfig, DomainDeployProvider } from "../domainInterface.js"; import { type DeletedRecordSet } from "./route53Release.js"; /** * The cascade's [G2]×D5 record-takeover gate (P6): the engine-side floor of * the ceremony the standalone `fjall domain deploy` command already runs. * Promoted here because review established the gate existed ONLY on that one * CLI command path — the cascade (`provider.deployDomain` → raw CDK deploy) * had no collision preflight on ANY surface, so an imported zone merged and * org-deployed would hit raw CloudFormation "record set already exists" with * no consent ceremony (the H5 incident class D5 exists to prevent). * * Shape: assess EVERY eligible domain first (synth → live-record preflight), * union the verified collisions into ONE cascade-scoped ticket, evaluate * consents ONCE (the strict evaluator's surplus rule stays intact — a * consent naming no ticket row fails the whole set), and only then release + * deploy. Withheld ⇒ the phase mutates NOTHING. The standalone CLI command * keeps its richer per-domain interactive ceremony; this gate is the floor, * not a replacement. */ /** * Everything the gate needs from the cascade's execution context. This is the * EXECUTING identity's context (plus the cascade-scoped consent inputs) — * `deployDomains` widens it into one `DomainsTakeoverGroup` per home when a * `DomainProviderFactory` routes foreign-pinned domains (T2). */ export interface DomainsTakeoverContext { aws: AwsProvider; cfnService: Pick; /** Flag-supplied consents (worker re-queue / CLI `--remediate`). */ consents?: readonly DestructionConsent[]; /** Digest the consents were minted against (TOCTOU binding). */ consentDigest?: string; /** Interactive resolver — consulted only when no flag consents came. */ consentGate?: DestructionConsentGate; /** Injectable Route53 client for the release arm (tests). */ route53Client?: Pick; abortSignal?: AbortSignal; } /** * One home's slice of the gate (T2 account routing): its domains are synthed * by ITS provider, their deployed templates read through ITS CloudFormation * service, and consented releases deleted through ITS Route53 credentials — * a foreign zone's records exist only in the foreign account, so running any * arm through the executing identity would probe/mutate the wrong account. * Consent stays cascade-scoped (`CascadeTakeoverConsentContext`): the * operator answers ONE ticket for the whole cascade, however many homes it * spans. */ export interface DomainsTakeoverGroup { /** The home this group deploys into — for diagnostics grammar. */ readonly home: { readonly accountId?: string; readonly region?: string; }; readonly provider: DomainDeployProvider; readonly aws: AwsProvider; readonly cfnService: Pick; /** Injectable Route53 client for this group's release arm (tests). */ readonly route53Client?: Pick; readonly domains: readonly DomainConfig[]; } /** The cascade-scoped consent inputs — shared across every group. */ export interface CascadeTakeoverConsentContext { readonly consents?: readonly DestructionConsent[]; readonly consentDigest?: string; readonly consentGate?: DestructionConsentGate; readonly abortSignal?: AbortSignal; } export interface DomainsTakeoverWithheld { readonly ticket: DestructionTicket; readonly outcome: DestructionOutcome; readonly report?: DestructionConsentReport; readonly reasons: readonly string[]; } export type DomainsTakeoverGateOutcome = { readonly kind: "clear"; readonly released: readonly DeletedRecordSet[]; } | { readonly kind: "withheld"; readonly withheld: DomainsTakeoverWithheld; }; /** Digest seed: deterministic across the mint run and the consent re-run. */ export declare function cascadeTakeoverDigestSeed(zoneNames: readonly string[]): string; /** * Mint one ticket resource per verified collision — the SAME spelling and * verb vocabulary as the standalone CLI ticket (`recordConsentSpelling`), so * the operator copies consents verbatim between surfaces. */ export declare function buildCascadeTakeoverTicket(zoneNames: readonly string[], collisions: readonly Route53RecordCollision[]): DestructionTicket; /** * Run the full gate: assess → (on verified, all-consentable collisions) * consent ceremony → consented release. Mirrors the CLI's * `runDomainConsentCeremony` semantics exactly: flag consents evaluated * engine-side, the interactive gate consulted ONLY when no flags came, an * `abort` verb is an operator withhold, and no blanket flag exists. * * A withhold DECIDES and RETURNS — the ticket, outcome, per-name verdict * report and reasons travel on `DomainsTakeoverWithheld` and onward onto * `DeployResult` (`orgCascadeDeploy`), where the surface renders them. Same * reason as the application gate: the withhold is the run's result, and * warning prose reaches stderr, where a caller reading only the result never * learns why its consents were refused. * * T2 account routing: assessment and release run PER GROUP through each * group's own credentials, but the ticket unions every group's collisions * and consent is evaluated ONCE against it — one cascade, one ceremony, * however many homes the domains span. Groups assess sequentially (in the * caller's order) so warning output stays deterministic, and one group's * fail-closed assessment refuses the WHOLE phase: a cascade that cannot * assess every eligible domain must not deploy any of them. */ export declare function runCascadeTakeoverGate(groups: readonly DomainsTakeoverGroup[], callbacks: DeployCallbacks, context: CascadeTakeoverConsentContext): Promise>;