import { type OrganisationBinding } from "../config.js"; /** * Project ↔ organisation binding: the offline-pure half. * * Sibling of `enforceDomainAccountPin` (the CLI's * `operations/domain/domainConfigRegistration`) one level up the stack. That guard pins a domain to the AWS ACCOUNT it was * created in; this one pins a project to the control-plane ORGANISATION it * belongs to. Same skeleton — committed config records a home, a resolved * identity is compared, an unreadable config fails closed, genuine absence * fails open — because the failure it prevents is the same shape: an * operation that silently lands somewhere nobody intended. * * Everything here is pure: it takes an already-resolved identity and reads * one file. Resolving the identity (and deciding what to do about each * verdict) belongs to the callers — the CLI's * `commands/organisationPrerequisite.ts` and the MCP server's * `organisationGate.ts` — which is why this lives in `@fjall/util` (its own * subpath, not the root barrel: `Config` reads the filesystem). */ /** The organisation the ambient credential actually names. */ export interface ResolvedOrganisationIdentity { readonly id: string; readonly name?: string; } /** * Five arms, not a boolean, for the reason `authPrerequisite` is three-valued: * "could not check" and "checked and it does not match" are different facts * with opposite correct responses, and collapsing them yields either a gate * that refuses on a flaky network or one that approves a real mismatch. * * `unverifiable` is not a hole. A refusal here is destructive to the * operator's intent — it stops work that may be entirely correct — so it * needs POSITIVE proof of mismatch, never mere absence of proof of match. */ export type OrganisationBindingVerdict = /** * Nothing to enforce. `projectFound` distinguishes "a project exists here * and records no binding" — the one case worth nudging — from "there is no * project here at all", where a nudge would name a project that does not * exist yet. `identity` is the organisation the caller resolved for the * comparison (null when it could not be, or was not needed): carried so a * creation flow can stamp the binding at birth from the SAME identity the * gate compared, rather than re-resolving and possibly stamping an * organisation the gate never saw. */ { readonly kind: "unbound"; readonly projectFound: boolean; readonly identity: ResolvedOrganisationIdentity | null; } | { readonly kind: "matched"; readonly bound: OrganisationBinding; } | { readonly kind: "mismatched"; readonly bound: OrganisationBinding; readonly actual: ResolvedOrganisationIdentity; } /** Bound, but the credential's organisation could not be resolved. */ | { readonly kind: "unverifiable"; readonly bound: OrganisationBinding; readonly reason: string; } /** A config file exists but its contents are not readable. */ | { readonly kind: "unreadable"; readonly reason: string; }; /** * What the project on disk says, before any identity is involved. * * Split from the comparison so the gate can decide whether resolving the * credential's organisation is worth doing at all: outside a project there is * nothing to compare and nothing to nudge about, so no identity is needed — * and an unreadable config is refused before a network call is spent on it. */ export type ProjectBindingRead = /** No fjall-config.json here or in any parent. */ { readonly kind: "no_project"; } /** A project exists and records no organisation. */ | { readonly kind: "unbound"; } | { readonly kind: "bound"; readonly bound: OrganisationBinding; } /** A config file exists but its contents are not readable. */ | { readonly kind: "unreadable"; readonly reason: string; }; /** * Read this project's recorded organisation. `startDir` overrides the * config-discovery start directory (tests). */ export declare function readProjectBinding(startDir?: string): ProjectBindingRead; /** * Turn what the project records and what the credential resolved to into a * verdict. `resolved === null` means the identity could not be established at * all — the caller says why via `unresolvedReason`. */ export declare function resolveBindingVerdict(read: ProjectBindingRead, resolved: ResolvedOrganisationIdentity | null, unresolvedReason?: string): OrganisationBindingVerdict; /** * Compare this project's recorded organisation against the one the ambient * credential names — the read and the comparison in one call, for callers * that already hold the identity. */ export declare function checkProjectOrganisation(resolved: ResolvedOrganisationIdentity | null, options?: { readonly startDir?: string; readonly unresolvedReason?: string; }): OrganisationBindingVerdict; /** How the bound organisation reads in a message: "Acme (org_abc)" or "org_abc". */ export declare function describeOrganisation(organisation: OrganisationBinding | ResolvedOrganisationIdentity): string; /** * The refusal. * * ONE cure, deliberately. The domain pin's inherited copy offers a second * ("update or remove the keys in fjall-config.json … a successful deploy * re-records them"), which reads as a bypass manual and — worse — teaches * exactly the ambient re-record that put a wrong organisation in the file in * the first place. Here the only documented move is `fjall org bind --force`, * which requires the operator to NAME the organisation they mean. */ export declare function buildWrongOrganisationMessage(action: string, bound: OrganisationBinding, actual: ResolvedOrganisationIdentity): string; /** The refusal when a config file exists but cannot be trusted to be empty. */ export declare function buildUnreadableConfigMessage(action: string, reason: string): string; /** * The notice printed when a bound project's organisation could not be * resolved. Availability-first, matching `requireAuthenticated`'s posture on * the same failure class: the operator is told exactly what was not checked. */ export declare function buildUnverifiableNotice(bound: OrganisationBinding, reason: string): string; /** * The one-line nudge shown on a gated surface in an UNBOUND project whose * organisation did resolve. This is the whole adoption path: existing * projects are unbound, and nothing else would ever tell their owners the * guard exists. */ export declare function buildUnboundNotice(actual: ResolvedOrganisationIdentity): string;