import type { Result } from "@fjall/generator"; import type { AwsProvider } from "../aws/AwsProvider.js"; import type { DeployCallbacks } from "../types/callbacks.js"; import type { DelegatedDomainDeployer } from "./domain/delegatedDomainDeploy.js"; import type { DomainsTakeoverWithheld } from "./domain/domainTakeoverGate.js"; /** * Configuration for a single domain to deploy. */ export interface DomainConfig { name: string; type: "apex" | "delegated"; /** * Config-declared parent zone of a delegated entry. `deployDomains` * orders delegated entries into parent-first waves on this key: a parent * that is itself a delegated entry in the same cascade must publish its * delegation-role export before its children deploy. Absent (or naming a * zone outside the entry set): no intra-cascade ordering edge - wave 0. */ parentDomain?: string; /** * Self-recorded home: the 12-digit AWS account id (and region) the last * successful deploy of this domain ran in, from the provider's config * entry. The cascade runs under ONE ambient identity, so `deployDomains` * SKIPS any domain whose pin contradicts the executing identity rather * than deploying it into the wrong account (the development.fjall.io * incident). Absent on never-deployed / pre-3.9.0 entries: fail-open. */ account?: string; region?: string; } /** * Result of a domain deployment phase. */ export interface DomainDeployResult { domainsDeployed: number; /** Domains left untouched because their account/region pin names another * home. A skip is expected topology, never an entry in `errors`. */ domainsSkipped: number; errors: string[]; /** * Zone names that actually deployed this run (P6): the per-zone retirement * signal for merged-not-deployed change rows. Skipped, withheld and failed * zones never appear here — a blanket "org deploy succeeded" flip can * never be honest for domain rows, only this list can. */ deployedZones: readonly string[]; /** * Present when the [G2]×D5 record-takeover gate withheld consent: the * phase deployed NOTHING (assess-first, mutate-never-before-consent) and * the caller must surface the ticket through its own consent machinery * (the worker's destructive_pending flow, the CLI's exit-4 ceremony). */ takeover?: DomainsTakeoverWithheld; } /** * Provider interface for domain deployment operations. * * Domain deployment requires CLI-specific services (zone file parsing, CDK * synthesis in domain component directories). deploy-core defines the cascade * structure; the caller provides the implementation. * * If no provider is given, the domains phase is skipped. */ export interface DomainDeployProvider { /** * Get the list of configured domains for this organisation. * Returns an empty array if no domains are configured. */ getDomains(): DomainConfig[]; /** * Deploy a single domain. Apex domains create hosted zones and delegation * roles; delegated domains create NS records in the parent zone. */ deployDomain(domainName: string, callbacks: DeployCallbacks): Promise>; /** * Optional phase-aware seam for delegated child domains (D8). Returning a * deployer routes the domain through the engine's two-phase orchestration * (`deployDelegatedDomain`: zone deploy → bounded NS-propagation gate → * CAA preflight → full deploy) instead of the single-shot `deployDomain`. * Return `undefined` (or omit the method) for domains that carry no * child-writes delegation — they keep the legacy single-shot path. */ delegatedDeployer?(domainName: string): DelegatedDomainDeployer | undefined; /** * Synthesise ONE domain's cloud assembly for the cascade's [G2]×D5 * record-takeover gate (P6): the returned stack names + assembly dir feed * `runRoute53RecordPreflight` BEFORE the phase mutates anything. * * Optional for interface compatibility, but NOT optional in effect: the * cascade FAILS the domains phase CLOSED when eligible domains exist and * the provider lacks this seam — an implementation that cannot be * collision-assessed never deploys ungated (the takeover ceremony must * not silently evaporate into a raw deploy). */ synthForTakeover?(domainName: string, callbacks: DeployCallbacks): Promise>; } /** * The resolved home of a domain group: each pin key defaulted to the * cascade's executing identity, so two entries pinning only `account` to the * same value land in one group. Always fully resolved — the factory never * sees a partial pin. */ export interface DomainHome { readonly accountId: string; readonly region: string; } /** * Group-scoped execution context minted by a `DomainProviderFactory`: the * provider deploys INTO the home, and the AWS provider holds credentials FOR * it (the takeover gate's template reads and record releases run through this, * never through the cascade's executing identity). Derived services * (CloudFormationService) are constructed engine-side from `aws`. */ export interface DomainProviderGroup { readonly provider: DomainDeployProvider; readonly aws: AwsProvider; } /** * T2 account routing: mint a provider context for a home OTHER than the one * the cascade executes under. `deployDomains` calls this once per distinct * foreign `(account, region)` pin among the configured domains — the caller * decides HOW to reach the home (worker: per-account OIDC role; CLI: * OrganizationAccountAccessRole from a management identity). A failure * degrades that home's domains to the pre-routing skip-with-warning, never to * a phase error: routing is an upgrade over the skip, not a new way to fail. */ export type DomainProviderFactory = (home: DomainHome) => Promise>; /** Assembly facts the takeover preflight reads for one domain. */ export interface DomainTakeoverSynth { /** Stack artifact names whose templates claim Route53 record sets. */ readonly stackNames: readonly string[]; /** ABSOLUTE path to the synthesised cloud assembly (`cdk.out`). */ readonly cdkOutPath: string; }