import { type Result } from "./docker/result.js"; import { type AccountTier } from "./environments.js"; import { type DomainConfig, type OrganisationBinding, type RootConfig } from "./rootConfigSchema.js"; export { DomainConfigSchema, OrganisationBindingSchema, ROOT_CONFIG_KEYS, RootConfigReadSchema, RootConfigSchema, serialiseRootConfig, type DomainConfig, type OrganisationBinding, type RootConfig, type RootConfigRead } from "./rootConfigSchema.js"; export { mergeRootConfig, mergeRootConfigContent, readRootConfigForMerge, type RootConfigDiskState, type RootConfigMergeRead, type RootConfigMergeResult } from "./rootConfigMerge.js"; export { LOCAL_CONFIG_DIRNAME, LOCAL_CONFIG_FILENAME, ROOT_CONFIG_FILENAME } from "./configPaths.js"; /** * The Fjall root config filename. Single source of truth — the repo-root marker * in findRepoRoot.ts MUST match this exactly or root detection diverges. Only * this exact name is discovered; sibling `.old` / `.backup` copies are inert. */ /** * Backup Vault Lock modes for an account's DisasterRecovery vault. * See decisions/2026-06-03-backup-vault-lock-mode-by-intent.md. */ export declare const VAULT_LOCK_MODES: readonly ["compliance", "governance", "none"]; export type VaultLockMode = (typeof VAULT_LOCK_MODES)[number]; /** * Account-level S3 Block Public Access posture. Fjall observes-and-flags public * buckets through the posture layer rather than hard-blocking, so enforcement is * off by default; "enforced" opts an account into all four account-level flags. */ export declare const S3_BPA_MODES: readonly ["enforced", "off"]; export type S3BpaMode = (typeof S3_BPA_MODES)[number]; /** * Org-level centralised root-access management mode (ADR * 2026-06-10-centralised-root-access-default-on). Absent ⇒ "centralised" * (default-on); "off" maps to OrgSetupConfig.skipRootAccessManagement at the * adapter boundaries (CLI buildOrgSetupConfig, webapp setup route). The * webapp re-exports this tuple at webapp/app/.server/constants/rootAccess.ts * (P2-T11, TRAIL_LIFECYCLE_STATES precedent). */ export declare const ROOT_ACCESS_MANAGEMENT_MODES: readonly ["centralised", "off"]; export type RootAccessManagementMode = (typeof ROOT_ACCESS_MANAGEMENT_MODES)[number]; /** * Per-account management-events-trail lifecycle for the organisation-trail * migration. Absent ⇒ legacy per-account trail, auto-eligible for migration * once org-trail delivery is verified. "account" pins the per-account trail * (the explicit reverse path). "draining" removes the trail resource while * retaining its bucket + CMK. "org" is terminal: the organisation trail * covers the account and local storage has been decommissioned. */ export declare const TRAIL_LIFECYCLE_STATES: readonly ["account", "draining", "org"]; export type TrailLifecycleState = (typeof TRAIL_LIFECYCLE_STATES)[number]; /** * Trail names shared between the CDK constructs * (@fjall/components-infrastructure) and the deploy-core org-trail migration — * SDK probes (DescribeTrails) must match what the constructs synthesise. */ export declare const ACCOUNT_TRAIL_NAME = "managementEvents"; export declare const ORGANISATION_TRAIL_NAME = "organisationManagementEvents"; /** * CDK-side trail-state vocabulary (the `fjallAccountTrailState` context * value), shared between the CDK constructs and deploy-core's context * builders. Distinct from TRAIL_LIFECYCLE_STATES: config intent * ("account"/"draining"/"org") maps onto synth behaviour * ("active"/"draining"/"removed"). */ export declare const ACCOUNT_TRAIL_STATES: readonly ["active", "draining", "removed"]; export type AccountTrailState = (typeof ACCOUNT_TRAIL_STATES)[number]; /** * Stack output keys (CfnOutput key + exportName) emitted by the Account / * Organisation CDK patterns and read back by the deploy-core org-trail * migration reconciler — both sides must use the same literals. */ export declare const TRAIL_BUCKET_OUTPUT_KEY = "FjallTrailBucketName"; export declare const TRAIL_KEY_ARN_OUTPUT_KEY = "FjallTrailKeyArn"; export declare const ORG_TRAIL_BUCKET_OUTPUT_KEY = "OrganisationTrailBucketName"; export type ProviderAccount = { id: string; name: string; /** * Workload STAGE — null for structural accounts (organisation/platform tiers * carry no workload stage). Read the structural axis from `tier`, never from * this field. */ environment: string | null; /** * Structural TIER (organisation/platform/account). Optional for wire * back-compat: old configs omit it and readers fall back to decoding * `environment` via `accountTier()`. Producers (webapp buildOrgConfigPayload) * emit it explicitly so the tier no longer rides on the environment string. */ tier?: AccountTier; managed?: boolean; oidcRoleArn?: string; /** * Home region for this account's infrastructure. Defaults to the org's * primaryRegion when absent. Keys the cascade's per-account IPAM pool * lookup (accountId+region), so it must be stamped before cascade deploy. */ region?: string; /** * Backup Vault Lock mode for this account's DisasterRecovery vault. Defaults * to "governance" (lock-protected but removable by privileged IAM). * "compliance" is permanently immutable after a 3-day cooling-off and * requires acknowledgeImmutableVaultLock. "none" disables the lock. */ vaultLock?: VaultLockMode; /** Explicit acknowledgement that vaultLock: "compliance" is irreversible. */ acknowledgeImmutableVaultLock?: boolean; /** * Account-level S3 Block Public Access posture. Absent ⇒ "off" — Fjall does * not enforce account-level BPA (the posture layer flags exposed buckets * instead). "enforced" sets all four account-level public-access flags true. */ s3BlockPublicAccess?: S3BpaMode; /** * Management-events-trail lifecycle for the org-trail migration. Absent ⇒ * legacy per-account trail (auto-eligible); see TRAIL_LIFECYCLE_STATES. */ trailLifecycle?: TrailLifecycleState; /** * Explicit acknowledgement that decommissioning the per-account trail * discards its retained log history. Bucket deletion never proceeds without * it — back up the bucket first if the history matters. */ acknowledgeTrailHistoryLoss?: boolean; }; /** * Assertion-only PUT entry for the org-config provider-accounts write. The * API preserves every axis the payload does not carry (absent ≠ null: null * clears the stage, absent leaves the stored value alone), so producers push * only the fields they actually know — fabricating a name or a null stage for * an unknown account overwrites curated registry state. */ export type ProviderAccountUpdate = Pick & Partial>; export type Profile = { type: "sso" | "oidc"; region: string; ssoAccountId?: string; ssoRoleName?: string; ssoSession?: string; oidcRoleArn?: string; oidcProviderArn?: string; roleArn?: string; roleSessionName?: string; }; export type SSOSession = { ssoRegion: string; ssoStartUrl: string; }; /** * Config class for loading and saving the root fjall-config.json. * Single config file at fjall/fjall-config.json (or fjall-config.json at cwd). * Stores active target and domains. * Org-level config (accounts, primary/DR regions, OIDC) is served by OrgConfigClient from the API. */ export declare class Config { rootConfig: RootConfig; private configPath; /** * True when the config file was FOUND on disk but could not be read. * saveConfig refuses to write in that state — the in-memory config never * included the real file's contents, so writing would clobber them. */ private loadFailed; /** * True when the file's JSON parsed but failed even the tolerant * RootConfigReadSchema (a recognised key such as `domains` is malformed), * so the WHOLE config fell back to empty and any recorded domain pins * vanished from memory. */ private parseDegraded; /** * Deep copy of the state this instance LOADED from disk (empty for a * programmatically-constructed Config, whose entire state is session * intent). saveConfig diffs the live state against this snapshot so it * only re-asserts what this session actually changed - an untouched key * (or an untouched domains[] entry) keeps whatever a concurrent process * wrote to disk between load and save. This is the structural fix for * the activeTarget lost-update incident: previously every save re-wrote * the whole load-time snapshot at top-level-key granularity, clobbering * a concurrent `fjall target set` and erasing concurrently-registered * sibling domain entries. */ private loadedSnapshot; /** * The directory `loadConfig` was asked to walk up from, remembered so an * UNBOUND instance stays coherent with its origin: later resolutions * (`resolveProjectDirectory`'s fallback walk, `saveConfig`'s create-a-new- * project default) anchor here instead of re-walking from process.cwd(). * Without this, `loadConfig(scaffoldRoot)` under `create app --into` * followed by a save could materialise the config in a DIFFERENT project — * whichever one happened to contain the shell's cwd. Undefined for * programmatically-constructed instances and no-arg loads (cwd semantics * unchanged). */ private searchStartDir; constructor(rootConfig?: RootConfig, configPath?: string); /** * Find the config directory by walking up the directory tree. * Looks for fjall/fjall-config.json or direct fjall-config.json. * Walks up from `startDir` when provided, otherwise from process.cwd(). */ private static findConfigDirectory; private static loadConfigFile; /** * Load the config, walking up from `startDir` (default process.cwd()). */ static loadConfig(startDir?: string): Config; private static formatZodError; saveConfig(): Result; /** * Writability is checked at save time, not load time: POSIX rename replaces * a read-only destination whenever the directory is writable, so without * this guard the tmp-plus-rename write would silently replace a chmod-444 * config. */ private static assertWritable; /** * Session-intent merge: re-reads the on-disk config immediately before * writing and asserts ONLY what this session actually changed relative to * its load-time snapshot (see loadedSnapshot). The rules live in * `mergeRootConfig` (`./rootConfigMerge.ts`), shared with the webapp * scaffold so the file's two writers cannot drift apart. * An absent file (first-ever save) writes the in-memory state as-is; an * unreadable or invalid existing file REFUSES the save — the in-memory * state came through the tolerant stripping read, so writing it unmerged * would silently delete every key the merge could not see. */ private mergeWithDisk; /** * Filesystem half of the pre-save re-read: an absent file needs no merge, an * unreadable one refuses the save, and the bytes of anything else are * classified by `readRootConfigForMerge`. */ private static readDiskConfigForMerge; static getConfigDirectory(startDir?: string): string | null; /** * Absolute path of the config file this instance was loaded from, or null * when no fjall-config.json was found — the "not inside a fjall project" * signal error paths branch on. */ getConfigPath(): string | null; /** * True when a fjall-config.json EXISTS on disk but its contents are not * represented in this instance: the file could not be read (loadFailed), * or its recognised keys were malformed and the tolerant read fell back * to empty (parseDegraded). Callers that enforce recorded truth - the * deploy account pin - must fail closed in this state rather than treat * a missing entry as the absence of a pin. False for a config that simply * does not exist. */ isContentUnavailable(): boolean; private static warnedLegacyActiveTarget; /** * In-memory view of `.fjall/local.json`, lazily loaded once per instance. * Reads serve from here so `set` then `get` is coherent within a process * and hot-path resolution never re-hits disk; writes flush through it. */ private localConfigCache; /** * Directory of the project this instance is bound to, or null when it is * bound to none. SINGLE SOURCE for every path derived from the project * root. * * `fjall-config.json` and `.fjall/local.json` MUST resolve to the same * root. They previously derived it independently and disagreed by one * level — `saveConfig` defaulted to `/fjall`, `localConfigFile` to * `` — so an unbound instance (first `fjall connect`, before the * project marker exists) wrote them to different roots and the local * write was invisible to every later read. */ private resolveProjectDirectory; /** * Absolute path of `.fjall/local.json`, or null when this instance is not * bound to a project. Never falls back to `process.cwd()`: per-checkout * state anchored to the shell's working directory lands somewhere no * later read looks. */ private localConfigFile; private loadLocalConfig; /** * Persist `.fjall/local.json`. Fails — rather than inventing a location — * when the instance is not bound to a project: a write anchored to the * shell's cwd is indistinguishable from success to the caller and * invisible to every subsequent read. */ private writeLocalConfig; private static warnLegacyActiveTargetOnce; getActiveTarget(): string | undefined; setActiveTarget(name: string): Result; clearActiveTarget(): Result; getDomains(): DomainConfig[]; setDomains(domains: DomainConfig[]): void; addDomain(domain: DomainConfig): void; getDomain(name: string): DomainConfig | undefined; removeDomain(name: string): boolean; getOrganisation(): OrganisationBinding | undefined; setOrganisation(binding: OrganisationBinding): void; clearOrganisation(): void; }