import { type Result } from "@fjall/generator"; import type { DeployParams } from "../../types/params.js"; import type { OrgConfig } from "../../types/config/orgConfig.js"; import type { DeployCallbacks } from "../../types/callbacks.js"; import type { OrganisationOperation } from "../../types/operations.js"; import type { DeployServices } from "../serviceFactory.js"; import type { DomainConfig, DomainDeployProvider, DomainDeployResult, DomainProviderFactory } from "../domainInterface.js"; import { type DomainsTakeoverContext } from "../domain/domainTakeoverGate.js"; import type { ProviderAccount } from "@fjall/util/config"; /** * Max member accounts deployed/destroyed in parallel during a cascade. Each * task spawns a `cdk` process, assumes a role, synthesises and deploys — so the * ceiling is the worker host's process/memory budget, NOT an AWS API limit * (the accounts are distinct, so CloudFormation/STS quotas don't contend). * Shared by deploy and destroy so they fan out identically. */ export declare const CASCADE_MAX_CONCURRENCY = 4; /** * Partition provider accounts into platform and member accounts. * Used by both deploy and destroy orchestration. */ export declare function partitionAccounts(providerAccounts: ProviderAccount[]): { platformAccount: ProviderAccount | undefined; memberAccounts: ProviderAccount[]; }; /** A single account paired with the region it deploys/destroys in. */ export interface AccountRegionPair { account: ProviderAccount; region: string; } /** * Build the full list of cascade regions (primary + secondary + DR, deduped). * * Single source of truth for "all org regions" — deploy and destroy MUST agree * on this set, else destroy sweeps regions deploy never created (or vice versa). */ export declare function buildRegionList(orgConfig: OrgConfig | undefined): string[]; /** * The cascade's home (anchor) region — first entry of buildRegionList, i.e. * the configured primary (or DEFAULT_REGION when none is set). Deploy and * destroy fan-outs MUST share this derivation so a destroy synth mirrors the * account-globals decision of the deploy that created each region's stack. */ export declare function cascadeHomeRegion(orgConfig: OrgConfig | undefined): string; /** * Operation key identifying one account x region task across the cascade * lifecycle. Deploy events, destroy events, and the Ink pre-seeded pills * (CLI `stackOperationUtils.fetchCascadeAccounts`) MUST share this shape so * every pill matches its engine events one-for-one. */ export declare function cascadeOperationKey(accountName: string, region: string): string; /** * Build all account x region pairs for parallel cascade fan-out. */ export declare function buildAccountRegionPairs(accounts: ProviderAccount[], regions: string[]): AccountRegionPair[]; export { buildCascadeRoleArn } from "../contextHelpers.js"; /** A single pre-flight probe failure: the cascade role could not be assumed. */ export interface CascadeRoleProbeFailure { accountId: string; accountName: string; error: string; } /** * Pre-flight check that OrganizationAccountAccessRole is assumable in every * cascade target, run BEFORE any synth/bootstrap/deploy work. An imported * (invite-joined) member never gets the role auto-created, so without this * probe a broken role only surfaces mid-cascade — after the org root has * already deployed — one account at a time. Read-only: the assumed * credentials are discarded. Inherits assumeCascadeRole's retry budget, so * role propagation on freshly created accounts does not false-fail. */ export declare function probeCascadeRoles(services: DeployServices, accounts: ProviderAccount[], callbacks: DeployCallbacks, signal?: AbortSignal): Promise; /** * Deploy a single cascade account (platform or member). * Assumes the target account's role, sets env credentials, and deploys. */ export interface CascadeAccountResult { outputs?: Record; /** * True when the synthesised stack was byte-identical to the last successful * deploy and the stack still exists, so bootstrap + deploy were skipped. */ skipped?: boolean; } export interface DeployCascadeAccountOptions { ipamPoolId?: string; /** * Effective org config carrying the reconciled provider accounts. The cascade * context must serialise THIS (not the possibly-stale `params.orgConfig`) so the * synthesised app's `getConfig()` can resolve every account by name/id. */ orgConfig?: OrgConfig; /** Region to deploy in; falls back to account.region then the provider region. */ region?: string; /** * Resolved primary (home) region of the cascade. buildParamsContext derives * the account-globals skip from it: non-home (secondary/DR) regions skip the * fixed-name org-global IAM resources that would collide across regions. * Omitted ⇒ globals are never skipped. */ primaryRegion?: string; /** * AWS Organizations org id — stamped as the owner cost-allocation tag on * the account's CDKToolkit stack at bootstrap * (`buildBootstrapTagsIfResolved`). Omitted on an org-classified estate ⇒ * the bootstrap emits NO tags (the complete set includes owner; a partial * set would strip it, so the gate preserves instead). Ignored when the * effective org config carries no organisation-tier account — the estate * classifies solo and owner must match what the application path can * re-derive. */ orgId?: string; } export declare function deployCascadeAccount(params: DeployParams, services: DeployServices, operation: OrganisationOperation, account: ProviderAccount, deployType: "platform" | "account", callbacks: DeployCallbacks, options?: DeployCascadeAccountOptions): Promise>; /** * Read Platform stack outputs to extract IPAM pool IDs for member accounts. * Output keys follow the pattern `IpamPoolId{12-digit-accountId}{regionSuffix}`. * Returns a map keyed by `{accountId}-{regionSuffix}` → pool ID. * Non-fatal: returns empty map on any error. */ export declare function readPlatformIpamPoolIds(services: DeployServices, platformAccount: { id: string; name: string; }, callbacks: DeployCallbacks, signal?: AbortSignal): Promise>; /** * Options threaded from the cascade into the delegated-domain orchestration. * P2 shipped `deployDomains` carrying neither: a non-interactive cascade * hitting a restrictive CAA chain stopped naming `--skip-caa-preflight` — a * flag no cascade surface could register because the pre-answer had no path * into `deployDelegatedDomain`. `DeployCallbacks` stays untouched — these are * inputs, not events. */ export interface DeployDomainsOptions { /** Caller cancellation (CLI Ctrl-C / worker SIGTERM). */ abortSignal?: AbortSignal; /** * Pre-answer for the CAA preflight (fail-closed ADR shape) — forwarded to * `deployDelegatedDomain`, where the skip is loud, never silent. */ skipCaaPreflight?: boolean; /** * The ambient identity this cascade executes under. When present, any * domain whose self-recorded `account`/`region` pin (DomainConfig) names a * different home is SKIPPED with a warning instead of deployed — the * account-blind domains phase is how development.fjall.io was deployed * into production. Absent (legacy callers): no partition, today's * behaviour. */ executingAccountId?: string; executingRegion?: string; /** * Execution context for the [G2]×D5 record-takeover gate (P6). NOT * optional in effect: when eligible domains exist and this is absent, the * phase FAILS CLOSED — the cascade must never deploy a domain whose * record claims were not collision-assessed (the gate is the floor on * every provider, CLI and worker alike). */ takeover?: DomainsTakeoverContext; /** * T2 account routing: mints a group-scoped provider context per distinct * foreign `(account, region)` pin, so pinned domains DEPLOY into their * recorded homes instead of being skipped — the "." * children the registry knows about stop depending on which account the * operator happened to run the cascade from. Absent: today's * skip-with-warning partition, byte for byte. A factory failure for one * home degrades THAT home's domains to the same skip — routing is an * upgrade over the skip, never a new way to fail the phase. */ domainProviderFactory?: DomainProviderFactory; /** * F5 standalone lane: restrict the phase to exactly these zone names * (compared through `normaliseRecordName`, so trailing dots and case * differences still match). The filter applies AFTER the config read and * BEFORE the partition, gate and wave plan — a filtered run keeps every * invariant of the full phase, just over fewer zones. Fail-closed * throughout: an EMPTY filter refuses (a caller wiring defect, not a * request to deploy nothing), a name matching no registered domain * refuses pre-AWS, and a requested zone the pin partition would skip * (foreign home, no routable group) FAILS the phase instead of the * cascade's skip-with-warning — in every case a silent no-op would read * as a successful deploy. Absent: every registered domain, today's * behaviour. */ zones?: readonly string[]; } /** * Order delegated domains into parent-first topological waves keyed on * `parentDomain` depth WITHIN the supplied set: a delegated entry whose * parent is another delegated entry deploys one wave after it (the parent * must publish its delegation-role export before the child's NS delegation * can be written). Parents outside the set - apex, foreign-account-skipped, * unregistered or externally-owned - carry no intra-set ordering edge, so * their children sit in wave 0; a flat topology therefore collapses to a * single wave (today's behaviour). A `parentDomain` cycle has no valid * order: fail closed naming the cycle so the caller can refuse pre-AWS. * Input order is preserved within each wave. */ export declare function planDelegatedDomainWaves(delegatedDomains: readonly DomainConfig[]): Result; /** * Deploy configured domains: apex domains sequentially, then delegated * domains in parent-first topological waves (parallel within a wave). * Delegates to the caller-provided DomainDeployProvider. * * P6: the [G2]×D5 record-takeover gate runs FIRST — every eligible domain is * synthed and collision-assessed before ANY domain mutates, so a withheld * ceremony deploys nothing (assess-first mirrors the application path's * withhold-before-mutation semantics). `deployedZones` names exactly the * zones that deployed — the only honest per-zone retirement signal. * * T2 account routing: with `options.domainProviderFactory`, domains pinned to * a foreign `(account, region)` home deploy through a home-scoped provider * context instead of being skipped. The partition, the takeover gate and the * wave plan all stay GLOBAL — one pin check, one consent ceremony, one * parent-first order across every home — while synth, template reads, record * releases and deploys run through each domain's own group. No factory (or a * factory failure for a home) keeps the skip-with-warning behaviour. */ export declare function deployDomains(provider: DomainDeployProvider, callbacks: DeployCallbacks, options?: DeployDomainsOptions): Promise;