/** * Identity-pin migration orchestration (D9 Phase 3) — ONE engine path shared * by both entry points: the deploy-flow pin arm (a consented `pin` on * rename-detected rows) and the standalone `fjall migrate identity` command. * * Per pinnable slot: decode the deployed anchor from the rename pair's * logical IDs → capture the verbatim identity surfaces from the live * template → re-synth with the candidate pins injected as CDK context * (NEVER written to disk first) → run the I2 identity-surface gate over * EVERY pinned slot (candidates AND pre-existing pins) → only then hand the * pin entries to the caller-owned config writer. Every operation lands a * terminal `pin` journal (phase `done` from birth — nothing is resumable; * a refused pin mutated nothing) recording per-slot outcomes. * * Messages are pre-masked at construction: engine-native failures are * identifier-only (logical IDs, anchors, slot names), and every seam error * interpolated from a caller-supplied closure (live-template read, pinned * re-synth, config write, journal write) routes through * `maskSensitiveOutput` here — sinks may surface them directly. */ import { type Result } from "@fjall/generator"; import type { CapacityIdentityPin, CapacityIdentityWire } from "@fjall/util"; import type { ManifestCapacityAlias } from "@fjall/util/manifest"; import type { RenameSlotHint } from "../../types/destruction.js"; import type { DeployPlan } from "../application/plan/types.js"; import { type PinnableRenamePair } from "./pinIdentity.js"; import { type RemediationJournalOptions } from "./remediationJournal.js"; /** One pinnable slot derived from the plan's hint-carrying rename rows. */ export interface IdentityPinCandidate { appName: string; slot: string; stackName: string; clusterName: string; /** The slot's CURRENT anchor (what the unpinned synth minted). */ anchor: string; pairs: PinnableRenamePair[]; } /** * Group the plan's pinnable rename rows into per-slot candidates. A row is * pinnable ⇔ it is a DELETE-side `rename-detected` row carrying the slot * hint matcher (a) attached (alias testimony names the {app, slot}); the * alias supplies the cluster name and current anchor. Rows whose hint names * an alias the manifest no longer carries are dropped — the hint travels * with the plan, so a stale plan against a changed manifest fails closed. */ export declare function collectIdentityPinCandidates(plan: Pick, aliases: readonly ManifestCapacityAlias[]): IdentityPinCandidate[]; /** Per-slot outcome of one identity-pin run. */ export interface IdentityPinSlotOutcome { appName: string; slot: string; stackName: string; legacyAnchor: string; /** The pin entry (written when `applied`, candidate otherwise). */ pin: CapacityIdentityPin; applied: boolean; /** Present ⇔ not applied — the identifier-only refusal. */ failureDetail?: string; /** I2 gate inequalities, verbatim (subset of failureDetail's cause). */ gateFailures?: string[]; } /** Pre-existing pinned slot re-verified by the standalone sweep. */ export interface IdentityPinVerifiedSlot { appName: string; slot: string; stackName?: string; ok: boolean; detail?: string; } export interface IdentityPinReport { outcomes: IdentityPinSlotOutcome[]; /** Pre-existing pins swept by the I2 gate (standalone entry). */ verified: IdentityPinVerifiedSlot[]; /** True ⇔ every candidate applied and every pre-existing pin verified. */ clean: boolean; } /** * Live-template lookup — distinguishes a DELETED stack from a failed read. * Candidates fail closed on `found: false` (a pin can only be proven against * a deployed identity); the pre-existing-pin sweep treats it as a clean skip * with the pin retained. */ export type LiveTemplateLookup = { found: true; template: unknown; } | { found: false; }; export interface RunIdentityPinMigrationParams { candidates: readonly IdentityPinCandidate[]; /** Manifest aliases — pre-existing pinned slots join the gate sweep. */ identityAliases: readonly ManifestCapacityAlias[]; /** Live deployed template per stack (CloudFormation GetTemplate). */ getLiveTemplate: (stackName: string) => Promise>; /** * Re-synth the assembly with the candidate pins injected as the * `fjall:capacityIdentity` context wire (merged over any existing pins by * the caller — the caller owns config projection). Returns the * synthesised template per stack name. */ synthWithPins: (wire: CapacityIdentityWire) => Promise, Error>>; /** * Caller-owned config write (fjall-config.json `capacityIdentity` * section). Invoked ONLY after every candidate passed the I2 gate; a * write failure downgrades every candidate to not-applied. */ writePins: (outcomes: readonly IdentityPinSlotOutcome[]) => Promise>; /** Existing wire pins to merge under the candidates for the re-synth. */ existingPins?: CapacityIdentityWire; /** Journal identity — omitted (tests) skips journalling. */ journal?: { accountId: string; region: string; appName?: string; options?: RemediationJournalOptions; now?: () => Date; }; onLog?: (message: string) => void; /** * Caller shutdown signal — pre-checked between per-slot iterations so a * SIGTERM/Ctrl-C does not sit behind the remaining slots' template reads. * Slots not yet processed land as not-applied/not-verified with an abort * detail (the report stays truthful; nothing is silently skipped). */ abortSignal?: AbortSignal; } /** * Degraded per-slot outcomes for a surface that could not execute the * migration at all (no runner injected, the operation failed before * collecting candidates, or a rejecting implementer). One outcome per * DISTINCT {app, slot, stack} — a multi-resource rename slot yields several * ticket rows carrying the same hint, and downstream renderers key on the * rendered line. The empty `legacyAnchor` is the journal/renderer "unknown" * sentinel. `failureDetail` must be pre-masked by the caller. */ export declare function degradedIdentityPinOutcomes(rows: ReadonlyArray<{ stack: string; pinSlotHint?: RenameSlotHint; }>, fallbackAppName: string, failureDetail: string): IdentityPinSlotOutcome[]; /** * Run the identity-pin migration over the collected candidates. * * Never throws; per-slot failures land in the report (`applied: false` with * `failureDetail`) so one refusing slot does not abandon the others' * verdicts — but the CONFIG WRITE is all-or-nothing: any candidate failing * the I2 gate withholds every write (a partial pin set changes the next * plan's shape mid-ceremony). The journal records the operation either way. */ export declare function runIdentityPinMigration(params: RunIdentityPinMigrationParams): Promise; /** Renderer pair: one line per slot outcome + verified sweep line. */ export declare function renderIdentityPinReportLines(report: IdentityPinReport): string[];