/** * [G2] Route53 record-collision pre-flight (domain gold-plating D5, audit * H5's deploy-side half). Before a deploy executes, every * AWS::Route53::RecordSet the synthesised template claims is diffed against * the live zone MINUS the stack's own previously deployed template: a live * record the incoming template claims but the previous template did not own * blocks the deploy BEFORE any mutation, naming the record, its live values * and the cure. CloudFormation's own failure for this class is a raw * mid-deploy "record set already exists" rollback — this gate turns it into * a pre-mutation stop with ownership guidance. * * Collision key: (name, type, setIdentifier) — two record sets sharing * (name, type) with DISTINCT SetIdentifiers are legal Route53 routing-policy * siblings (latency/weighted) and pass. Route53 additionally forbids a * simple record (no SetIdentifier) coexisting with ANY record at the same * (name, type), in either direction — both mixed shapes are collisions here * because Route53 rejects them at ChangeResourceRecordSets. * * Fail-closed contract (fail-closed ADR): a live-zone listing or * previous-template read that FAILS blocks the deploy — an unverifiable zone * must never fall through to "assume free", and a blocked outcome carries * structured `collisions` ONLY when EVERY blocking problem is a verified * live-record collision (unverifiable state must never become takeover-able). * There is deliberately NO bypass flag: the terminating cures are (a) retry * when the failure was transient, (b) inspect ownership with 'fjall domain * records list ', (c) for a record another stack owns, release it from * that owner through the destruction ceremony first, and (d) for an adopted * zone whose live records this stack is meant to take over, re-run * "fjall domain deploy " and consent to the takeover ticket with * --remediate (the ceremony releases each named record so this deploy's * stack recreates it from the declaration). * * Two sanctioned fail-OPEN branches (structural, per the fail-closed ADR's * exception clause — the open branch provably cannot destroy, because a real * collision then still fails at CloudFormation's own already-exists refusal * before any live record is touched; the degradation is back to the pre-D5 * raw-error UX, never a silent takeover): * - an unreadable synthesised template (claims unknowable) skips that stack * with a warn, matching `synthTemplateDeclaresResource`'s degradation — * but ONLY under the default `onUnreadableSynthTemplate: "skip"`. A * caller that just synthesised the assembly itself (the domain-deploy * takeover gate) knows an unreadable template is a wiring defect, not a * legacy degradation, and selects `"block"`: the unreadable template * becomes a fail-closed problem (structured collisions suppressed) so the * takeover ceremony can never silently evaporate; * - a claim whose Name / zone identity is a CFN intrinsic rather than a * literal (legacy non-D2 synth) is skipped with a warn naming it. */ import type { AwsProvider } from "../../aws/AwsProvider.js"; import type { CloudFormationService } from "../../services/infrastructure/CloudFormationService.js"; export interface Route53RecordPreflightParams { stackNames: readonly string[]; /** cdk.out assembly dir holding `${stackName}.template.json`. */ cdkOutPath: string; cfnService: Pick; onLog?: (message: string, level: "info" | "warn") => void; /** * What an unreadable synthesised template does to the outcome. * `"skip"` (default) is the sanctioned fail-open (module header): warn and * let CloudFormation's already-exists refusal backstop the deploy. * `"block"` is for callers that OWN the synth they are reading (the * domain-deploy takeover gate): the unreadable template is a fail-closed * problem — blocked, with structured collisions suppressed. */ onUnreadableSynthTemplate?: "skip" | "block"; abortSignal?: AbortSignal; } /** * One verified live-record collision, structured for the takeover ceremony * (`fjall domain deploy` mints a DestructionTicket row per collision). */ export interface Route53RecordCollision { /** Stack whose synthesised template claims the record. */ readonly stackName: string; readonly logicalId: string; /** The template's own spelling of the record name (for messages). */ readonly declaredName: string; /** deploy-core canonical name (lowercased, octal-unescaped, trailing dot). */ readonly canonicalName: string; readonly type: string; /** Present when the template claim carries a routing-policy SetIdentifier. */ readonly setIdentifier?: string; /** * SetIdentifier of the LIVE record set this row would release — distinct * from the claim's own `setIdentifier`. Each colliding live variant mints * its own row, so a takeover release names exactly one record set. */ readonly liveSetIdentifier?: string; /** Bare hosted-zone id the live collision was found in. */ readonly zoneId: string; /** Pre-masked human description of the live record's values. */ readonly liveDescription: string; /** Whether a takeover ceremony may release this record. */ readonly consentable: boolean; /** Cure-naming reason when the row can never be consented. */ readonly nonConsentableReason?: string; } export type Route53RecordPreflightOutcome = { kind: "proceed"; } | { kind: "blocked"; message: string; /** * Present ONLY when EVERY blocking problem is a verified live-record * collision. Any fail-closed problem (previous-template parse/fetch * failure, unlistable zone, abort) suppresses the array entirely — * unverifiable state must never become takeover-able (fail-closed ADR). */ collisions?: readonly Route53RecordCollision[]; }; export declare function runRoute53RecordPreflight(aws: AwsProvider, params: Route53RecordPreflightParams): Promise;